From 6e88aae091c15994a6857bf43707a4e6496c3223 Mon Sep 17 00:00:00 2001 From: dsinghvi Date: Wed, 24 Jul 2024 09:59:20 -0400 Subject: [PATCH] run seed --- .../alias/tests/unit/zurg/date/date.test.ts | 31 ++ .../alias/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../alias/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../alias/tests/unit/zurg/lazy/recursive/a.ts | 7 + .../alias/tests/unit/zurg/lazy/recursive/b.ts | 8 + .../alias/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../alias/tests/unit/zurg/schema.test.ts | 78 +++++ .../alias/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../alias/tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../alias/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../alias/tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/api-wide-base-path/package.json | 6 +- seed/ts-sdk/audiences/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../audiences/tests/unit/zurg/schema.test.ts | 78 +++++ .../audiences/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../auth-environment-variables/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/basic-auth/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../basic-auth/tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/code-samples/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/custom-auth/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/enum/package.json | 6 +- .../enum/tests/unit/zurg/date/date.test.ts | 31 ++ .../enum/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../enum/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../enum/tests/unit/zurg/lazy/recursive/a.ts | 7 + .../enum/tests/unit/zurg/lazy/recursive/b.ts | 8 + .../enum/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../enum/tests/unit/zurg/schema.test.ts | 78 +++++ .../enum/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../enum/tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../enum/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../enum/tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../error-property/union-utils/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../examples-with-api-reference/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../retain-original-casing/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../allow-extra-fields/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/exhaustive/bundle/package.json | 6 +- .../bundle/tests/unit/zurg/date/date.test.ts | 31 ++ .../bundle/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../bundle/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../bundle/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../bundle/tests/unit/zurg/schema.test.ts | 78 +++++ .../bundle/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../bundle/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../custom-package-json/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../exhaustive/dev-dependencies/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/exhaustive/jsr/package.json | 6 +- .../jsr/tests/unit/zurg/date/date.test.ts | 31 ++ .../jsr/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../jsr/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../jsr/tests/unit/zurg/lazy/recursive/a.ts | 7 + .../jsr/tests/unit/zurg/lazy/recursive/b.ts | 8 + .../jsr/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../jsr/tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../jsr/tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../jsr/tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../jsr/tests/unit/zurg/schema.test.ts | 78 +++++ .../jsr/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../jsr/tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../jsr/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../jsr/tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../exhaustive/no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../retain-original-casing/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../extends/tests/unit/zurg/date/date.test.ts | 31 ++ .../extends/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../extends/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../extends/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../extends/tests/unit/zurg/schema.test.ts | 78 +++++ .../extends/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../extends/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/extra-properties/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../package.json | 6 +- .../no-custom-config/package.json | 6 +- .../file-upload/no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../wrap-file-properties/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/folders/package.json | 6 +- .../folders/tests/unit/zurg/date/date.test.ts | 31 ++ .../folders/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../folders/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../folders/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../folders/tests/unit/zurg/schema.test.ts | 78 +++++ .../folders/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../folders/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/idempotency-headers/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/imdb/package.json | 19 +- seed/ts-sdk/literal/package.json | 6 +- .../literal/tests/unit/zurg/date/date.test.ts | 31 ++ .../literal/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../literal/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../literal/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../literal/tests/unit/zurg/schema.test.ts | 78 +++++ .../literal/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../literal/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../mixed-case/no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../retain-original-casing/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/multi-line-docs/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../ts-sdk/multi-url-environment/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/no-environment/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../never-throw-errors/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../oauth-client-credentials/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../object/tests/unit/zurg/date/date.test.ts | 31 ++ .../object/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../object/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../object/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../object/tests/unit/zurg/schema.test.ts | 78 +++++ .../object/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../object/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/optional/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../optional/tests/unit/zurg/schema.test.ts | 78 +++++ .../optional/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/package-yml/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/pagination/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../pagination/tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/plain-text/package.json | 6 +- .../no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../no-serde-layer-query/package.json | 6 +- seed/ts-sdk/reserved-keywords/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/response-property/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/server-sent-events/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../allow-custom-fetcher/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../streaming/no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/trace/exhaustive/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../exhaustive/tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../trace/no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../trace/no-zurg-no-throwing/package.json | 6 +- seed/ts-sdk/trace/no-zurg-trace/package.json | 6 +- .../no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../skip-response-validation/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/unions/package.json | 6 +- .../unions/tests/unit/zurg/date/date.test.ts | 31 ++ .../unions/tests/unit/zurg/enum/enum.test.ts | 30 ++ .../unions/tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../unions/tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../unions/tests/unit/zurg/schema.test.ts | 78 +++++ .../unions/tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../unions/tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../unknown/no-custom-config/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ .../unknown/unknown-as-any/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/validation/package.json | 6 +- .../tests/unit/zurg/date/date.test.ts | 31 ++ .../tests/unit/zurg/enum/enum.test.ts | 30 ++ .../tests/unit/zurg/lazy/lazy.test.ts | 60 ++++ .../tests/unit/zurg/lazy/lazyObject.test.ts | 20 ++ .../tests/unit/zurg/lazy/recursive/a.ts | 7 + .../tests/unit/zurg/lazy/recursive/b.ts | 8 + .../tests/unit/zurg/list/list.test.ts | 43 +++ .../unit/zurg/literals/stringLiteral.test.ts | 21 ++ .../object-like/withParsedProperties.test.ts | 60 ++++ .../tests/unit/zurg/object/extend.test.ts | 92 ++++++ .../tests/unit/zurg/object/object.test.ts | 266 ++++++++++++++++++ .../objectWithoutOptionalProperties.test.ts | 23 ++ .../tests/unit/zurg/primitives/any.test.ts | 6 + .../unit/zurg/primitives/boolean.test.ts | 14 + .../tests/unit/zurg/primitives/number.test.ts | 14 + .../tests/unit/zurg/primitives/string.test.ts | 14 + .../unit/zurg/primitives/unknown.test.ts | 6 + .../tests/unit/zurg/record/record.test.ts | 35 +++ .../zurg/schema-utils/getSchemaUtils.test.ts | 55 ++++ .../validation/tests/unit/zurg/schema.test.ts | 78 +++++ .../tests/unit/zurg/set/set.test.ts | 49 ++++ .../tests/unit/zurg/skipValidation.test.ts | 45 +++ .../undiscriminatedUnion.test.ts | 46 +++ .../tests/unit/zurg/union/union.test.ts | 116 ++++++++ .../tests/unit/zurg/utils/itSchema.ts | 78 +++++ .../tests/unit/zurg/utils/itValidate.ts | 56 ++++ seed/ts-sdk/variables/package.json | 6 +- 1623 files changed, 76638 insertions(+), 133 deletions(-) create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/alias/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/audiences/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/auth-environment-variables/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/basic-auth/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/circular-references-advanced/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/circular-references/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/code-samples/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/custom-auth/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/enum/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/error-property/union-utils/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/extends/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/extra-properties/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/folders/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/idempotency-headers/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/literal/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/multi-line-docs/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/multi-url-environment/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/no-environment/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/object/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/objects-with-imports/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/optional/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/package-yml/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/pagination/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/reserved-keywords/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/response-property/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/server-sent-events/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/single-url-environment-default/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/trace/exhaustive/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/unions/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/utils/itValidate.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/date/date.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/enum/enum.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/lazy/lazy.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/lazy/recursive/a.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/lazy/recursive/b.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/list/list.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/object/extend.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/object/object.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/primitives/any.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/primitives/boolean.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/primitives/number.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/primitives/string.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/primitives/unknown.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/record/record.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/schema.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/set/set.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/skipValidation.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/union/union.test.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/utils/itSchema.ts create mode 100644 seed/ts-sdk/validation/tests/unit/zurg/utils/itValidate.ts diff --git a/seed/ts-sdk/alias/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/alias/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/alias/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/alias/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/alias/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/alias/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/alias/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/alias/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/alias/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/api-wide-base-path/package.json b/seed/ts-sdk/api-wide-base-path/package.json index a357ec3f2df..58865488ef9 100644 --- a/seed/ts-sdk/api-wide-base-path/package.json +++ b/seed/ts-sdk/api-wide-base-path/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/audiences/package.json b/seed/ts-sdk/audiences/package.json index 63f177084e8..b90f8183504 100644 --- a/seed/ts-sdk/audiences/package.json +++ b/seed/ts-sdk/audiences/package.json @@ -19,11 +19,13 @@ }, "devDependencies": { "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/audiences/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/audiences/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/audiences/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/audiences/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/audiences/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/audiences/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/audiences/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/auth-environment-variables/package.json b/seed/ts-sdk/auth-environment-variables/package.json index 95ca5f19c74..d385af29fa9 100644 --- a/seed/ts-sdk/auth-environment-variables/package.json +++ b/seed/ts-sdk/auth-environment-variables/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/auth-environment-variables/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/basic-auth-environment-variables/package.json b/seed/ts-sdk/basic-auth-environment-variables/package.json index 6105fddab5a..10820d2d4d2 100644 --- a/seed/ts-sdk/basic-auth-environment-variables/package.json +++ b/seed/ts-sdk/basic-auth-environment-variables/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/basic-auth-environment-variables/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/basic-auth/package.json b/seed/ts-sdk/basic-auth/package.json index f3ff1d433dc..af4e7dd3ba1 100644 --- a/seed/ts-sdk/basic-auth/package.json +++ b/seed/ts-sdk/basic-auth/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/basic-auth/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/basic-auth/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/basic-auth/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/bearer-token-environment-variable/package.json b/seed/ts-sdk/bearer-token-environment-variable/package.json index d3e543226ac..e997fde5290 100644 --- a/seed/ts-sdk/bearer-token-environment-variable/package.json +++ b/seed/ts-sdk/bearer-token-environment-variable/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/bearer-token-environment-variable/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/circular-references-advanced/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/circular-references/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/circular-references/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/circular-references/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/code-samples/package.json b/seed/ts-sdk/code-samples/package.json index 41485b89bac..3636f76560e 100644 --- a/seed/ts-sdk/code-samples/package.json +++ b/seed/ts-sdk/code-samples/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/code-samples/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/code-samples/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/code-samples/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/custom-auth/package.json b/seed/ts-sdk/custom-auth/package.json index a915ac38106..f8e8dd6a01d 100644 --- a/seed/ts-sdk/custom-auth/package.json +++ b/seed/ts-sdk/custom-auth/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/custom-auth/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/custom-auth/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/custom-auth/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/enum/package.json b/seed/ts-sdk/enum/package.json index c031c0e10d8..6bb20afa30b 100644 --- a/seed/ts-sdk/enum/package.json +++ b/seed/ts-sdk/enum/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/enum/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/enum/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/enum/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/enum/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/enum/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/enum/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/enum/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/enum/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/enum/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/error-property/union-utils/package.json b/seed/ts-sdk/error-property/union-utils/package.json index ae25224f546..4398137a3f2 100644 --- a/seed/ts-sdk/error-property/union-utils/package.json +++ b/seed/ts-sdk/error-property/union-utils/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/error-property/union-utils/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/examples/examples-with-api-reference/package.json b/seed/ts-sdk/examples/examples-with-api-reference/package.json index f9ab5406ae6..ab34b81ab3f 100644 --- a/seed/ts-sdk/examples/examples-with-api-reference/package.json +++ b/seed/ts-sdk/examples/examples-with-api-reference/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/examples/examples-with-api-reference/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/examples/retain-original-casing/package.json b/seed/ts-sdk/examples/retain-original-casing/package.json index f9ab5406ae6..ab34b81ab3f 100644 --- a/seed/ts-sdk/examples/retain-original-casing/package.json +++ b/seed/ts-sdk/examples/retain-original-casing/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/examples/retain-original-casing/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/package.json b/seed/ts-sdk/exhaustive/allow-extra-fields/package.json index d3094a9d0ae..d1d47977f54 100644 --- a/seed/ts-sdk/exhaustive/allow-extra-fields/package.json +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/exhaustive/allow-extra-fields/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/exhaustive/bundle/package.json b/seed/ts-sdk/exhaustive/bundle/package.json index d243fb4f9ae..e7ea3f2bf89 100644 --- a/seed/ts-sdk/exhaustive/bundle/package.json +++ b/seed/ts-sdk/exhaustive/bundle/package.json @@ -51,11 +51,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "esbuild": "0.16.15", "prettier": "2.7.1", diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/exhaustive/bundle/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/exhaustive/custom-package-json/package.json b/seed/ts-sdk/exhaustive/custom-package-json/package.json index af7b47889ff..b7a4c1574b9 100644 --- a/seed/ts-sdk/exhaustive/custom-package-json/package.json +++ b/seed/ts-sdk/exhaustive/custom-package-json/package.json @@ -53,11 +53,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "esbuild": "0.16.15", "prettier": "2.7.1", diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/exhaustive/custom-package-json/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/package.json b/seed/ts-sdk/exhaustive/dev-dependencies/package.json index 2a09ffa48d9..077f42bd6cb 100644 --- a/seed/ts-sdk/exhaustive/dev-dependencies/package.json +++ b/seed/ts-sdk/exhaustive/dev-dependencies/package.json @@ -59,11 +59,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "^29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "esbuild": "0.16.15", "prettier": "2.7.1", diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/exhaustive/dev-dependencies/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/exhaustive/jsr/package.json b/seed/ts-sdk/exhaustive/jsr/package.json index d3094a9d0ae..d1d47977f54 100644 --- a/seed/ts-sdk/exhaustive/jsr/package.json +++ b/seed/ts-sdk/exhaustive/jsr/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/exhaustive/jsr/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/exhaustive/no-custom-config/package.json b/seed/ts-sdk/exhaustive/no-custom-config/package.json index d3094a9d0ae..d1d47977f54 100644 --- a/seed/ts-sdk/exhaustive/no-custom-config/package.json +++ b/seed/ts-sdk/exhaustive/no-custom-config/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/exhaustive/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/package.json b/seed/ts-sdk/exhaustive/retain-original-casing/package.json index d3094a9d0ae..d1d47977f54 100644 --- a/seed/ts-sdk/exhaustive/retain-original-casing/package.json +++ b/seed/ts-sdk/exhaustive/retain-original-casing/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/exhaustive/retain-original-casing/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/extends/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/extends/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/extends/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/extends/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/extends/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/extends/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/extends/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/extends/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/extends/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/extra-properties/package.json b/seed/ts-sdk/extra-properties/package.json index d3038eabde0..89be62be122 100644 --- a/seed/ts-sdk/extra-properties/package.json +++ b/seed/ts-sdk/extra-properties/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/extra-properties/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/extra-properties/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/extra-properties/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/file-download/file-download-reponse-headers/package.json b/seed/ts-sdk/file-download/file-download-reponse-headers/package.json index 3905321e040..474c8243535 100644 --- a/seed/ts-sdk/file-download/file-download-reponse-headers/package.json +++ b/seed/ts-sdk/file-download/file-download-reponse-headers/package.json @@ -19,11 +19,13 @@ }, "devDependencies": { "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/file-download/no-custom-config/package.json b/seed/ts-sdk/file-download/no-custom-config/package.json index 3905321e040..474c8243535 100644 --- a/seed/ts-sdk/file-download/no-custom-config/package.json +++ b/seed/ts-sdk/file-download/no-custom-config/package.json @@ -19,11 +19,13 @@ }, "devDependencies": { "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/file-upload/no-custom-config/package.json b/seed/ts-sdk/file-upload/no-custom-config/package.json index 18dd9884274..3367f69e1fe 100644 --- a/seed/ts-sdk/file-upload/no-custom-config/package.json +++ b/seed/ts-sdk/file-upload/no-custom-config/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/file-upload/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/package.json b/seed/ts-sdk/file-upload/wrap-file-properties/package.json index 18dd9884274..3367f69e1fe 100644 --- a/seed/ts-sdk/file-upload/wrap-file-properties/package.json +++ b/seed/ts-sdk/file-upload/wrap-file-properties/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/file-upload/wrap-file-properties/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/folders/package.json b/seed/ts-sdk/folders/package.json index f320976b358..f7a40e1ec7e 100644 --- a/seed/ts-sdk/folders/package.json +++ b/seed/ts-sdk/folders/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/folders/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/folders/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/folders/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/folders/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/folders/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/folders/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/folders/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/folders/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/folders/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/idempotency-headers/package.json b/seed/ts-sdk/idempotency-headers/package.json index 301645055af..2918ec4492a 100644 --- a/seed/ts-sdk/idempotency-headers/package.json +++ b/seed/ts-sdk/idempotency-headers/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/idempotency-headers/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/idempotency-headers/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/imdb/package.json b/seed/ts-sdk/imdb/package.json index 8d99c05d29f..f8236ee8d8d 100644 --- a/seed/ts-sdk/imdb/package.json +++ b/seed/ts-sdk/imdb/package.json @@ -1,6 +1,7 @@ { "name": "@fern/imdb", "version": "0.0.1", + "private": false, "repository": "https://github.com/imdb/fern", "main": "./index.js", "types": "./index.d.ts", @@ -11,25 +12,25 @@ "test": "jest" }, "dependencies": { + "url-join": "4.0.1", "form-data": "4.0.0", "formdata-node": "^6.0.3", - "js-base64": "3.7.2", "node-fetch": "2.7.0", "qs": "6.11.2", - "url-join": "4.0.1" + "js-base64": "3.7.2" }, "devDependencies": { - "@babel/preset-env": "^7.24.3", - "@babel/preset-typescript": "^7.24.1", + "@types/url-join": "4.0.1", + "@types/qs": "6.9.8", "@types/jest": "29.5.5", - "@types/node": "17.0.33", "@types/node-fetch": "2.6.9", - "@types/qs": "6.9.8", - "@types/url-join": "4.0.1", - "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "prettier": "2.7.1", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", + "jest": "29.7.0", "ts-jest": "29.1.1", + "@types/node": "17.0.33", + "prettier": "2.7.1", "typescript": "4.6.4" }, "browser": { diff --git a/seed/ts-sdk/literal/package.json b/seed/ts-sdk/literal/package.json index e51d94661ee..6ffd8dd1467 100644 --- a/seed/ts-sdk/literal/package.json +++ b/seed/ts-sdk/literal/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/literal/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/literal/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/literal/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/literal/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/literal/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/literal/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/literal/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/literal/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/literal/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/mixed-case/no-custom-config/package.json b/seed/ts-sdk/mixed-case/no-custom-config/package.json index b27ca0d6601..61eea421eb8 100644 --- a/seed/ts-sdk/mixed-case/no-custom-config/package.json +++ b/seed/ts-sdk/mixed-case/no-custom-config/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/mixed-case/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/package.json b/seed/ts-sdk/mixed-case/retain-original-casing/package.json index b27ca0d6601..61eea421eb8 100644 --- a/seed/ts-sdk/mixed-case/retain-original-casing/package.json +++ b/seed/ts-sdk/mixed-case/retain-original-casing/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/mixed-case/retain-original-casing/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/multi-line-docs/package.json b/seed/ts-sdk/multi-line-docs/package.json index f67cb3aee27..fee5cffb466 100644 --- a/seed/ts-sdk/multi-line-docs/package.json +++ b/seed/ts-sdk/multi-line-docs/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/multi-line-docs/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/multi-line-docs/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/multi-url-environment-no-default/package.json b/seed/ts-sdk/multi-url-environment-no-default/package.json index 72ce0a4e8af..0faa3679879 100644 --- a/seed/ts-sdk/multi-url-environment-no-default/package.json +++ b/seed/ts-sdk/multi-url-environment-no-default/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment-no-default/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/multi-url-environment/package.json b/seed/ts-sdk/multi-url-environment/package.json index 0135d5bf683..9645d564b59 100644 --- a/seed/ts-sdk/multi-url-environment/package.json +++ b/seed/ts-sdk/multi-url-environment/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/multi-url-environment/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/multi-url-environment/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/no-environment/package.json b/seed/ts-sdk/no-environment/package.json index 6026dc1a769..b75a0daa223 100644 --- a/seed/ts-sdk/no-environment/package.json +++ b/seed/ts-sdk/no-environment/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/no-environment/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/no-environment/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/no-environment/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials-default/package.json b/seed/ts-sdk/oauth-client-credentials-default/package.json index 6544de91516..c16a7471996 100644 --- a/seed/ts-sdk/oauth-client-credentials-default/package.json +++ b/seed/ts-sdk/oauth-client-credentials-default/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-default/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/package.json b/seed/ts-sdk/oauth-client-credentials-environment-variables/package.json index 08145fbe7a3..a16f4eb3b4c 100644 --- a/seed/ts-sdk/oauth-client-credentials-environment-variables/package.json +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-environment-variables/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/package.json b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/package.json index 26383db9f72..0b37d24258a 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/package.json +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/never-throw-errors/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/package.json b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/package.json index 26383db9f72..0b37d24258a 100644 --- a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/package.json +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials-nested-root/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials/package.json b/seed/ts-sdk/oauth-client-credentials/package.json index 3c92857ca59..4d3ef9ba748 100644 --- a/seed/ts-sdk/oauth-client-credentials/package.json +++ b/seed/ts-sdk/oauth-client-credentials/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/oauth-client-credentials/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/object/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/object/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/object/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/object/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/object/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/object/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/object/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/object/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/object/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/object/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/object/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/object/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/object/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/object/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/object/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/object/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/object/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/object/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/object/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/object/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/object/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/object/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/object/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/object/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/object/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/object/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/object/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/object/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/object/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/object/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/objects-with-imports/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/objects-with-imports/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/optional/package.json b/seed/ts-sdk/optional/package.json index 0ce65bdade8..e19da0b8b3e 100644 --- a/seed/ts-sdk/optional/package.json +++ b/seed/ts-sdk/optional/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/optional/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/optional/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/optional/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/optional/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/optional/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/optional/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/optional/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/optional/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/optional/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/package-yml/package.json b/seed/ts-sdk/package-yml/package.json index 1e3b7bd61cf..cdda4cde927 100644 --- a/seed/ts-sdk/package-yml/package.json +++ b/seed/ts-sdk/package-yml/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/package-yml/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/package-yml/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/package-yml/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/pagination/package.json b/seed/ts-sdk/pagination/package.json index 5d858d43327..9999a85ddf8 100644 --- a/seed/ts-sdk/pagination/package.json +++ b/seed/ts-sdk/pagination/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/pagination/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/pagination/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/pagination/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/pagination/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/pagination/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/pagination/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/pagination/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/plain-text/package.json b/seed/ts-sdk/plain-text/package.json index 29e2bade571..85113a05a31 100644 --- a/seed/ts-sdk/plain-text/package.json +++ b/seed/ts-sdk/plain-text/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/query-parameters/no-custom-config/package.json b/seed/ts-sdk/query-parameters/no-custom-config/package.json index f6113a730b0..4495b59e300 100644 --- a/seed/ts-sdk/query-parameters/no-custom-config/package.json +++ b/seed/ts-sdk/query-parameters/no-custom-config/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/query-parameters/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/query-parameters/no-serde-layer-query/package.json b/seed/ts-sdk/query-parameters/no-serde-layer-query/package.json index f6113a730b0..4495b59e300 100644 --- a/seed/ts-sdk/query-parameters/no-serde-layer-query/package.json +++ b/seed/ts-sdk/query-parameters/no-serde-layer-query/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/reserved-keywords/package.json b/seed/ts-sdk/reserved-keywords/package.json index 78ecb96075b..06e3e608f82 100644 --- a/seed/ts-sdk/reserved-keywords/package.json +++ b/seed/ts-sdk/reserved-keywords/package.json @@ -19,11 +19,13 @@ }, "devDependencies": { "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/reserved-keywords/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/reserved-keywords/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/response-property/package.json b/seed/ts-sdk/response-property/package.json index 8b96c1e51b1..0cee1568909 100644 --- a/seed/ts-sdk/response-property/package.json +++ b/seed/ts-sdk/response-property/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/response-property/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/response-property/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/response-property/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/response-property/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/response-property/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/response-property/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/response-property/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/server-sent-events/package.json b/seed/ts-sdk/server-sent-events/package.json index c658b07f61c..1cccb975464 100644 --- a/seed/ts-sdk/server-sent-events/package.json +++ b/seed/ts-sdk/server-sent-events/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/server-sent-events/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/server-sent-events/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/server-sent-events/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/single-url-environment-default/package.json b/seed/ts-sdk/single-url-environment-default/package.json index c61a44ffd34..760a42c104d 100644 --- a/seed/ts-sdk/single-url-environment-default/package.json +++ b/seed/ts-sdk/single-url-environment-default/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-default/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/single-url-environment-no-default/package.json b/seed/ts-sdk/single-url-environment-no-default/package.json index fdca307bdd9..bb1a2ece689 100644 --- a/seed/ts-sdk/single-url-environment-no-default/package.json +++ b/seed/ts-sdk/single-url-environment-no-default/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/single-url-environment-no-default/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/package.json b/seed/ts-sdk/streaming/allow-custom-fetcher/package.json index fe3d9e1510b..4a6a209bd34 100644 --- a/seed/ts-sdk/streaming/allow-custom-fetcher/package.json +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/streaming/allow-custom-fetcher/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/streaming/no-custom-config/package.json b/seed/ts-sdk/streaming/no-custom-config/package.json index fe3d9e1510b..4a6a209bd34 100644 --- a/seed/ts-sdk/streaming/no-custom-config/package.json +++ b/seed/ts-sdk/streaming/no-custom-config/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/streaming/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/trace/exhaustive/package.json b/seed/ts-sdk/trace/exhaustive/package.json index 8cdc9f8bf65..4d078ce2efd 100644 --- a/seed/ts-sdk/trace/exhaustive/package.json +++ b/seed/ts-sdk/trace/exhaustive/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/trace/exhaustive/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/trace/no-custom-config/package.json b/seed/ts-sdk/trace/no-custom-config/package.json index 8cdc9f8bf65..4d078ce2efd 100644 --- a/seed/ts-sdk/trace/no-custom-config/package.json +++ b/seed/ts-sdk/trace/no-custom-config/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/trace/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/trace/no-zurg-no-throwing/package.json b/seed/ts-sdk/trace/no-zurg-no-throwing/package.json index 8cdc9f8bf65..4d078ce2efd 100644 --- a/seed/ts-sdk/trace/no-zurg-no-throwing/package.json +++ b/seed/ts-sdk/trace/no-zurg-no-throwing/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/trace/no-zurg-trace/package.json b/seed/ts-sdk/trace/no-zurg-trace/package.json index 8cdc9f8bf65..4d078ce2efd 100644 --- a/seed/ts-sdk/trace/no-zurg-trace/package.json +++ b/seed/ts-sdk/trace/no-zurg-trace/package.json @@ -22,11 +22,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/package.json b/seed/ts-sdk/undiscriminated-unions/no-custom-config/package.json index 1a292689828..8eb7446aa88 100644 --- a/seed/ts-sdk/undiscriminated-unions/no-custom-config/package.json +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/package.json b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/package.json index 1a292689828..8eb7446aa88 100644 --- a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/package.json +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/undiscriminated-unions/skip-response-validation/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/unions/package.json b/seed/ts-sdk/unions/package.json index e93aa7a39c8..b0e66476dd6 100644 --- a/seed/ts-sdk/unions/package.json +++ b/seed/ts-sdk/unions/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/unions/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/unions/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/unions/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/unions/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/unions/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/unions/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/unions/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/unions/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/unions/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/unknown/no-custom-config/package.json b/seed/ts-sdk/unknown/no-custom-config/package.json index d0fd310b8b5..58b0793df24 100644 --- a/seed/ts-sdk/unknown/no-custom-config/package.json +++ b/seed/ts-sdk/unknown/no-custom-config/package.json @@ -19,11 +19,13 @@ }, "devDependencies": { "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/unknown/no-custom-config/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/unknown/unknown-as-any/package.json b/seed/ts-sdk/unknown/unknown-as-any/package.json index d0fd310b8b5..58b0793df24 100644 --- a/seed/ts-sdk/unknown/unknown-as-any/package.json +++ b/seed/ts-sdk/unknown/unknown-as-any/package.json @@ -19,11 +19,13 @@ }, "devDependencies": { "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/unknown/unknown-as-any/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/validation/package.json b/seed/ts-sdk/validation/package.json index 9987c4ed7c8..8af5578c071 100644 --- a/seed/ts-sdk/validation/package.json +++ b/seed/ts-sdk/validation/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4" diff --git a/seed/ts-sdk/validation/tests/unit/zurg/date/date.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/date/date.test.ts new file mode 100644 index 00000000000..2790268a09c --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/date/date.test.ts @@ -0,0 +1,31 @@ +import { date } from "../../../../src/core/schemas/builders/date"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("date", () => { + itSchema("converts between raw ISO string and parsed Date", date(), { + raw: "2022-09-29T05:41:21.939Z", + parsed: new Date("2022-09-29T05:41:21.939Z"), + }); + + itValidateParse("non-string", date(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateParse("non-ISO", date(), "hello world", [ + { + message: 'Expected ISO 8601 date string. Received "hello world".', + path: [], + }, + ]); + + itValidateJson("non-Date", date(), "hello", [ + { + message: 'Expected Date object. Received "hello".', + path: [], + }, + ]); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/enum/enum.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 00000000000..a7b25de6cb9 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { enum_ } from "../../../../src/core/schemas/builders/enum"; + +describe("enum", () => { + itSchemaIdentity(enum_(["A", "B", "C"]), "A"); + + itSchemaIdentity(enum_(["A", "B", "C"]), "D" as any, { + opts: { allowUnrecognizedEnumValues: true }, + }); + + itValidate("invalid enum", enum_(["A", "B", "C"]), "D", [ + { + message: 'Expected enum. Received "D".', + path: [], + }, + ]); + + itValidate( + "non-string", + enum_(["A", "B", "C"]), + [], + [ + { + message: "Expected string. Received list.", + path: [], + }, + ] + ); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/lazy/lazy.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 00000000000..cb67130fe48 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,60 @@ +import { lazy } from "../../../../src/core/schemas/builders/lazy"; +import { list } from "../../../../src/core/schemas/builders/list"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { Schema } from "../../../../src/core/schemas/Schema"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + it("doesn't run immediately", () => { + let wasRun = false; + lazy(() => { + wasRun = true; + return string(); + }); + expect(wasRun).toBe(false); + }); + + it("only runs first time", async () => { + let count = 0; + const schema = lazy(() => { + count++; + return string(); + }); + await schema.parse("hello"); + await schema.json("world"); + expect(count).toBe(1); + }); + + itSchemaIdentity( + lazy(() => object({})), + { foo: "hello" }, + { + title: "passes opts through", + opts: { unrecognizedObjectKeys: "passthrough" }, + } + ); + + itSchemaIdentity( + lazy(() => object({ foo: string() })), + { foo: "hello" } + ); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial schema doesn't compile", () => { + () => { + // @ts-expect-error + const a = lazy(() => object({ foo: a })); + }; + }); + + // eslint-disable-next-line jest/expect-expect + it("self-referencial compiles with explicit type", () => { + () => { + interface TreeNode { + children: TreeNode[]; + } + const TreeNode: Schema = lazy(() => object({ children: list(TreeNode) })); + }; + }); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/lazy/lazyObject.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 00000000000..774359bfcf4 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,20 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { lazyObject } from "../../../../src/core/schemas/builders/lazy/lazyObject"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("lazy", () => { + itSchemaIdentity( + lazyObject(() => object({ foo: string() })), + { foo: "hello" } + ); + + itSchemaIdentity( + lazyObject(() => object({ foo: string() })).extend(object({ bar: number() })), + { + foo: "hello", + bar: 42, + }, + { title: "returned schema has object utils" } + ); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/lazy/recursive/a.ts b/seed/ts-sdk/validation/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 00000000000..8b7d5e40cfa --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/lazy/recursive/a.ts @@ -0,0 +1,7 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { schemaB } from "./b"; + +// @ts-expect-error +export const schemaA = object({ + b: schemaB, +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/lazy/recursive/b.ts b/seed/ts-sdk/validation/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 00000000000..fb219d54c8e --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/lazy/recursive/b.ts @@ -0,0 +1,8 @@ +import { object } from "../../../../../src/core/schemas/builders/object"; +import { optional } from "../../../../../src/core/schemas/builders/schema-utils"; +import { schemaA } from "./a"; + +// @ts-expect-error +export const schemaB = object({ + a: optional(schemaA), +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/list/list.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/list/list.test.ts new file mode 100644 index 00000000000..a54d01c3641 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,43 @@ +import { list } from "../../../../src/core/schemas/builders/list"; +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("list", () => { + itSchemaIdentity(list(string()), ["hello", "world"], { + title: "functions as identity when item type is primitive", + }); + + itSchema( + "converts objects correctly", + list( + object({ + helloWorld: property("hello_world", string()), + }) + ), + { + raw: [{ hello_world: "123" }], + parsed: [{ helloWorld: "123" }], + } + ); + + itValidate("not a list", list(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidate( + "invalid item type", + list(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/literals/stringLiteral.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 00000000000..768b38943ec --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals/stringLiteral"; + +describe("stringLiteral", () => { + itSchemaIdentity(stringLiteral("A"), "A"); + + itValidate("incorrect string", stringLiteral("A"), "B", [ + { + path: [], + message: 'Expected "A". Received "B".', + }, + ]); + + itValidate("non-string", stringLiteral("A"), 42, [ + { + path: [], + message: 'Expected "A". Received 42.', + }, + ]); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/object-like/withParsedProperties.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 00000000000..f5b64763fae --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,60 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { object } from "../../../../src/core/schemas/builders/object/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { string } from "../../../../src/core/schemas/builders/primitives"; + +describe("withParsedProperties", () => { + it("Added properties included on parsed object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + printHelloWorld: () => () => "Hello world", + helloWorld: "Hello world", + }); + + const parsed = await schema.parse({ raw_foo: "value of foo", bar: "bar" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printFoo()).toBe("value of foo"); + expect(parsed.value.printHelloWorld()).toBe("Hello world"); + expect(parsed.value.helloWorld).toBe("Hello world"); + }); + + it("Added property is removed on raw object", async () => { + const schema = object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }).withParsedProperties({ + printFoo: (parsed) => () => parsed.foo, + }); + + const original = { raw_foo: "value of foo", bar: "bar" } as const; + const parsed = await schema.parse(original); + if (!parsed.ok) { + throw new Error("Failed to parse()"); + } + + const raw = await schema.json(parsed.value); + + if (!raw.ok) { + throw new Error("Failed to json()"); + } + + expect(raw.value).toEqual(original); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .withParsedProperties(42); + }); + }); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/object/extend.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 00000000000..36949628d8a --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,92 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { boolean, string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("extend", () => { + itSchemaIdentity( + object({ + foo: string(), + }).extend( + object({ + bar: stringLiteral("bar"), + }) + ), + { + foo: "", + bar: "bar", + } as const, + { + title: "extended properties are included in schema", + } + ); + + itSchemaIdentity( + object({ + foo: string(), + }) + .extend( + object({ + bar: stringLiteral("bar"), + }) + ) + .extend( + object({ + baz: boolean(), + }) + ), + { + foo: "", + bar: "bar", + baz: true, + } as const, + { + title: "extensions can be extended", + } + ); + + itSchema( + "converts nested object", + object({ + item: object({ + helloWorld: property("hello_world", string()), + }), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item: { hello_world: "yo" }, goodbye_raw: "peace" }, + parsed: { item: { helloWorld: "yo" }, goodbye: "peace" }, + } + ); + + itSchema( + "extensions work with raw/parsed property name conversions", + object({ + item: property("item_raw", string()), + }).extend( + object({ + goodbye: property("goodbye_raw", string()), + }) + ), + { + raw: { item_raw: "hi", goodbye_raw: "peace" }, + parsed: { item: "hi", goodbye: "peace" }, + } + ); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object schema", () => { + () => + object({ + foo: string(), + }) + // @ts-expect-error + .extend([]); + }); + }); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/object/object.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/object/object.test.ts new file mode 100644 index 00000000000..9eb148ea91e --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,266 @@ +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { any, number, string, unknown } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { property } from "../../../../src/core/schemas/builders/object/property"; + +describe("object", () => { + itSchemaIdentity( + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { + foo: "", + bar: "bar", + }, + { + title: "functions as identity when values are primitives and property() isn't used", + } + ); + + itSchema( + "uses raw key from property()", + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { raw_foo: "foo", bar: "bar" }, + parsed: { foo: "foo", bar: "bar" }, + } + ); + + itSchema( + "keys with unknown type can be omitted", + object({ + foo: unknown(), + }), + { + raw: {}, + parsed: {}, + } + ); + + itSchema( + "keys with any type can be omitted", + object({ + foo: any(), + }), + { + raw: {}, + parsed: {}, + } + ); + + describe("unrecognizedObjectKeys", () => { + describe("parse", () => { + itParse( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itParse( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + + describe("json", () => { + itJson( + 'includes unknown values when unrecognizedObjectKeys === "passthrough"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "passthrough", + }, + } + ); + + itJson( + 'strips unknown values when unrecognizedObjectKeys === "strip"', + object({ + foo: property("raw_foo", string()), + bar: stringLiteral("bar"), + }), + { + raw: { + raw_foo: "foo", + bar: "bar", + }, + parsed: { + foo: "foo", + bar: "bar", + // @ts-expect-error + baz: "yoyo", + }, + opts: { + unrecognizedObjectKeys: "strip", + }, + } + ); + }); + }); + + describe("nullish properties", () => { + itSchema("missing properties are not added", object({ foo: property("raw_foo", string().optional()) }), { + raw: {}, + parsed: {}, + }); + + itSchema("undefined properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + itSchema("null properties are not dropped", object({ foo: property("raw_foo", string().optional()) }), { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + }); + + describe("extensions", () => { + itSchema( + "undefined properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + + describe("parse()", () => { + itParse( + "null properties are not dropped", + object({}).extend(object({ foo: property("raw_foo", string().optional()) })), + { + raw: { raw_foo: null }, + parsed: { foo: undefined }, + } + ); + }); + }); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with non-object in schema", () => { + // @ts-expect-error + object([]); + }); + }); + + itValidate( + "missing property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello" }, + [ + { + path: [], + message: 'Missing required key "bar"', + }, + ] + ); + + itValidate( + "extra property", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + { foo: "hello", bar: "bar", baz: 42 }, + [ + { + path: ["baz"], + message: 'Unexpected key "baz"', + }, + ] + ); + + itValidate( + "not an object", + object({ + foo: string(), + bar: stringLiteral("bar"), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "nested validation error", + object({ + foo: object({ + bar: number(), + }), + }), + { foo: { bar: "hello" } }, + [ + { + path: ["foo", "bar"], + message: 'Expected number. Received "hello".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 00000000000..8eb9a20f183 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,23 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders/literals"; +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { objectWithoutOptionalProperties } from "../../../../src/core/schemas/builders/object/objectWithoutOptionalProperties"; +import { itSchema } from "../utils/itSchema"; + +describe("objectWithoutOptionalProperties", () => { + itSchema( + "all properties are required", + objectWithoutOptionalProperties({ + foo: string(), + bar: stringLiteral("bar").optional(), + }), + { + raw: { + foo: "hello", + }, + // @ts-expect-error + parsed: { + foo: "hello", + }, + } + ); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/primitives/any.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 00000000000..f34851ad939 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders/primitives/any"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/primitives/boolean.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 00000000000..991f0656eb1 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { boolean } from "../../../../src/core/schemas/builders/primitives/boolean"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/primitives/number.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 00000000000..f20b953a9a8 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { number } from "../../../../src/core/schemas/builders/primitives/number"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/primitives/string.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 00000000000..13cfe5e2f12 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; +import { string } from "../../../../src/core/schemas/builders/primitives/string"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/primitives/unknown.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 00000000000..452f8d14359 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders/primitives/unknown"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/record/record.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/record/record.test.ts new file mode 100644 index 00000000000..499ee8bc206 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,35 @@ +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { record } from "../../../../src/core/schemas/builders/record"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("record", () => { + itSchemaIdentity(record(string(), string()), { hello: "world" }); + itSchemaIdentity(record(number(), string()), { 42: "world" }); + + itValidate( + "non-record", + record(number(), string()), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate("invalid key type", record(number(), string()), { hello: "world" }, [ + { + path: ["hello (key)"], + message: 'Expected number. Received "hello".', + }, + ]); + + itValidate("invalid value type", record(string(), number()), { hello: "world" }, [ + { + path: ["hello"], + message: 'Expected number. Received "world".', + }, + ]); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 00000000000..a766ca013c1 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,55 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { object } from "../../../../src/core/schemas/builders/object"; +import { itSchema } from "../utils/itSchema"; + +describe("getSchemaUtils", () => { + describe("optional()", () => { + itSchema("optional fields allow original schema", string().optional(), { + raw: "hello", + parsed: "hello", + }); + + itSchema("optional fields are not required", string().optional(), { + raw: null, + parsed: undefined, + }); + }); + + describe("transform()", () => { + itSchema( + "transorm and untransform run correctly", + string().transform({ + transform: (x) => x + "X", + untransform: (x) => (x as string).slice(0, -1), + }), + { + raw: "hello", + parsed: "helloX", + } + ); + }); + + describe("parseOrThrow()", () => { + it("parses valid value", async () => { + const value = string().parseOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).parseOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); + + describe("jsonOrThrow()", () => { + it("serializes valid value", async () => { + const value = string().jsonOrThrow("hello"); + expect(value).toBe("hello"); + }); + + it("throws on invalid value", async () => { + const value = () => object({ a: string(), b: string() }).jsonOrThrow({ a: 24 }); + expect(value).toThrowError(new Error('a: Expected string. Received 24.; Missing required key "b"')); + }); + }); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/schema.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/schema.test.ts new file mode 100644 index 00000000000..94089a9a91b --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/schema.test.ts @@ -0,0 +1,78 @@ +import { + boolean, + discriminant, + list, + number, + object, + string, + stringLiteral, + union, +} from "../../../src/core/schemas/builders"; +import { booleanLiteral } from "../../../src/core/schemas/builders/literals/booleanLiteral"; +import { property } from "../../../src/core/schemas/builders/object/property"; +import { itSchema } from "./utils/itSchema"; + +describe("Schema", () => { + itSchema( + "large nested object", + object({ + a: string(), + b: stringLiteral("b value"), + c: property( + "raw_c", + list( + object({ + animal: union(discriminant("type", "_type"), { + dog: object({ value: boolean() }), + cat: object({ value: property("raw_cat", number()) }), + }), + }) + ) + ), + d: property("raw_d", boolean()), + e: booleanLiteral(true), + }), + { + raw: { + a: "hello", + b: "b value", + raw_c: [ + { + animal: { + _type: "dog", + value: true, + }, + }, + { + animal: { + _type: "cat", + raw_cat: 42, + }, + }, + ], + raw_d: false, + e: true, + }, + parsed: { + a: "hello", + b: "b value", + c: [ + { + animal: { + type: "dog", + value: true, + }, + }, + { + animal: { + type: "cat", + value: 42, + }, + }, + ], + d: false, + e: true, + }, + } + ); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/set/set.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/set/set.test.ts new file mode 100644 index 00000000000..4f80aa9f095 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,49 @@ +import { string } from "../../../../src/core/schemas/builders/primitives"; +import { set } from "../../../../src/core/schemas/builders/set"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("set", () => { + itSchema("converts between raw list and parsed Set", set(string()), { + raw: ["A", "B"], + parsed: new Set(["A", "B"]), + }); + + itValidateParse("not a list", set(string()), 42, [ + { + path: [], + message: "Expected list. Received 42.", + }, + ]); + + itValidateJson( + "not a Set", + set(string()), + [], + [ + { + path: [], + message: "Expected Set. Received list.", + }, + ] + ); + + itValidateParse( + "invalid item type", + set(string()), + [42], + [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ] + ); + + itValidateJson("invalid item type", set(string()), new Set([42]), [ + { + path: ["[0]"], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/skipValidation.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 00000000000..5dc88096a9f --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/skipValidation.test.ts @@ -0,0 +1,45 @@ +/* eslint-disable no-console */ + +import { boolean, number, object, property, string, undiscriminatedUnion } from "../../../src/core/schemas/builders"; + +describe("skipValidation", () => { + it("allows data that doesn't conform to the schema", async () => { + const warningLogs: string[] = []; + const originalConsoleWarn = console.warn; + console.warn = (...args) => warningLogs.push(args.join(" ")); + + const schema = object({ + camelCase: property("snake_case", string()), + numberProperty: number(), + requiredProperty: boolean(), + anyPrimitive: undiscriminatedUnion([string(), number(), boolean()]), + }); + + const parsed = await schema.parse( + { + snake_case: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + { + skipValidation: true, + } + ); + + expect(parsed).toEqual({ + ok: true, + value: { + camelCase: "hello", + numberProperty: "oops", + anyPrimitive: true, + }, + }); + + expect(warningLogs).toEqual([ + `Failed to validate. + - numberProperty: Expected number. Received "oops".`, + ]); + + console.warn = originalConsoleWarn; + }); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 00000000000..f6ecc3a480a --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,46 @@ +import { object, property } from "../../../../src/core/schemas/builders/object"; +import { number, string } from "../../../../src/core/schemas/builders/primitives"; +import { undiscriminatedUnion } from "../../../../src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; + +describe("undiscriminatedUnion", () => { + itSchemaIdentity(undiscriminatedUnion([string(), number()]), "hello world"); + + itSchemaIdentity(undiscriminatedUnion([object({ hello: string() }), object({ goodbye: string() })]), { + goodbye: "foo", + }); + + itSchema( + "Correctly transforms", + undiscriminatedUnion([object({ hello: string() }), object({ helloWorld: property("hello_world", string()) })]), + { + raw: { hello_world: "foo " }, + parsed: { helloWorld: "foo " }, + } + ); + + it("Returns errors for all variants", async () => { + const result = await undiscriminatedUnion([string(), number()]).parse(true); + if (result.ok) { + throw new Error("Unexpectedly passed validation"); + } + expect(result.errors).toEqual([ + { + message: "[Variant 0] Expected string. Received true.", + path: [], + }, + { + message: "[Variant 1] Expected number. Received true.", + path: [], + }, + ]); + }); + + describe("compile", () => { + // eslint-disable-next-line jest/expect-expect + it("doesn't compile with zero members", () => { + // @ts-expect-error + () => undiscriminatedUnion([]); + }); + }); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/union/union.test.ts b/seed/ts-sdk/validation/tests/unit/zurg/union/union.test.ts new file mode 100644 index 00000000000..9322ea3d5e4 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,116 @@ +import { object } from "../../../../src/core/schemas/builders/object"; +import { boolean, number, string } from "../../../../src/core/schemas/builders/primitives"; +import { union } from "../../../../src/core/schemas/builders/union"; +import { discriminant } from "../../../../src/core/schemas/builders/union/discriminant"; +import { itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("union", () => { + itSchemaIdentity( + union("type", { + lion: object({ + meows: boolean(), + }), + giraffe: object({ + heightInInches: number(), + }), + }), + { type: "lion", meows: true }, + { title: "doesn't transform discriminant when it's a string" } + ); + + itSchema( + "transforms discriminant when it's a discriminant()", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + raw: { _type: "lion", meows: true }, + parsed: { type: "lion", meows: true }, + } + ); + + describe("allowUnrecognizedUnionMembers", () => { + itSchema( + "transforms discriminant & passes through values when discriminant value is unrecognized", + union(discriminant("type", "_type"), { + lion: object({ meows: boolean() }), + giraffe: object({ heightInInches: number() }), + }), + { + // @ts-expect-error + raw: { _type: "moose", isAMoose: true }, + // @ts-expect-error + parsed: { type: "moose", isAMoose: true }, + opts: { + allowUnrecognizedUnionMembers: true, + }, + } + ); + }); + + describe("withParsedProperties", () => { + it("Added property is included on parsed object", async () => { + const schema = union("type", { + lion: object({}), + tiger: object({ value: string() }), + }).withParsedProperties({ + printType: (parsed) => () => parsed.type, + }); + + const parsed = await schema.parse({ type: "lion" }); + if (!parsed.ok) { + throw new Error("Failed to parse"); + } + expect(parsed.value.printType()).toBe("lion"); + }); + }); + + itValidate( + "non-object", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + [], + [ + { + path: [], + message: "Expected object. Received list.", + }, + ] + ); + + itValidate( + "missing discriminant", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + {}, + [ + { + path: [], + message: 'Missing discriminant ("type")', + }, + ] + ); + + itValidate( + "unrecognized discriminant value", + union("type", { + lion: object({}), + tiger: object({ value: string() }), + }), + { + type: "bear", + }, + [ + { + path: ["type"], + message: 'Expected enum. Received "bear".', + }, + ] + ); +}); diff --git a/seed/ts-sdk/validation/tests/unit/zurg/utils/itSchema.ts b/seed/ts-sdk/validation/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 00000000000..67b6c928175 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/utils/itSchema.ts @@ -0,0 +1,78 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions } from "../../../../src/core/schemas/Schema"; + +export function itSchemaIdentity( + schema: Schema, + value: T, + { title = "functions as identity", opts }: { title?: string; opts?: SchemaOptions } = {} +): void { + itSchema(title, schema, { raw: value, parsed: value, opts }); +} + +export function itSchema( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + only = false, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + only?: boolean; + } +): void { + // eslint-disable-next-line jest/valid-title + (only ? describe.only : describe)(title, () => { + itParse("parse()", schema, { raw, parsed, opts }); + itJson("json()", schema, { raw, parsed, opts }); + }); +} + +export function itParse( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.parse(raw, opts); + if (!maybeValid.ok) { + throw new Error("Failed to parse() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(parsed); + }); +} + +export function itJson( + title: string, + schema: Schema, + { + raw, + parsed, + opts, + }: { + raw: Raw; + parsed: Parsed; + opts?: SchemaOptions; + } +): void { + // eslint-disable-next-line jest/valid-title + it(title, () => { + const maybeValid = schema.json(parsed, opts); + if (!maybeValid.ok) { + throw new Error("Failed to json() " + JSON.stringify(maybeValid.errors, undefined, 4)); + } + expect(maybeValid.value).toStrictEqual(raw); + }); +} diff --git a/seed/ts-sdk/validation/tests/unit/zurg/utils/itValidate.ts b/seed/ts-sdk/validation/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 00000000000..75b2c08b036 --- /dev/null +++ b/seed/ts-sdk/validation/tests/unit/zurg/utils/itValidate.ts @@ -0,0 +1,56 @@ +/* eslint-disable jest/no-export */ +import { Schema, SchemaOptions, ValidationError } from "../../../../src/core/schemas/Schema"; + +export function itValidate( + title: string, + schema: Schema, + input: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + // eslint-disable-next-line jest/valid-title + describe("parse()", () => { + itValidateParse(title, schema, input, errors, opts); + }); + describe("json()", () => { + itValidateJson(title, schema, input, errors, opts); + }); +} + +export function itValidateParse( + title: string, + schema: Schema, + raw: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("parse", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.parse(raw, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} + +export function itValidateJson( + title: string, + schema: Schema, + parsed: unknown, + errors: ValidationError[], + opts?: SchemaOptions +): void { + describe("json", () => { + // eslint-disable-next-line jest/valid-title + it(title, async () => { + const maybeValid = await schema.json(parsed, opts); + if (maybeValid.ok) { + throw new Error("Value passed validation"); + } + expect(maybeValid.errors).toStrictEqual(errors); + }); + }); +} diff --git a/seed/ts-sdk/variables/package.json b/seed/ts-sdk/variables/package.json index 2d7d033ce80..a2e9a67ed27 100644 --- a/seed/ts-sdk/variables/package.json +++ b/seed/ts-sdk/variables/package.json @@ -21,11 +21,13 @@ "devDependencies": { "@types/url-join": "4.0.1", "@types/qs": "6.9.8", + "@types/jest": "29.5.5", "@types/node-fetch": "2.6.9", + "jest-environment-jsdom": "29.7.0", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "jest": "29.7.0", - "@types/jest": "29.5.5", "ts-jest": "29.1.1", - "jest-environment-jsdom": "29.7.0", "@types/node": "17.0.33", "prettier": "2.7.1", "typescript": "4.6.4"