From 7fc96a01010fe33859d9d18feaa43156b7233196 Mon Sep 17 00:00:00 2001 From: eden Date: Wed, 27 Nov 2024 16:43:06 -0500 Subject: [PATCH] v2 TS SDK with Websocket. --- README.md | 6 +- package.json | 24 +- .../tts/types/WebSocketStreamOptions.ts | 7 + .../tts/types/WebSocketTimestampsResponse.ts | 9 + src/core/schemas/Schema.ts | 99 ++++ src/core/schemas/builders/bigint/bigint.ts | 50 ++ src/core/schemas/builders/bigint/index.ts | 1 + src/core/schemas/builders/date/date.ts | 65 +++ src/core/schemas/builders/date/index.ts | 1 + src/core/schemas/builders/enum/enum.ts | 43 ++ src/core/schemas/builders/enum/index.ts | 1 + src/core/schemas/builders/index.ts | 14 + src/core/schemas/builders/lazy/index.ts | 3 + src/core/schemas/builders/lazy/lazy.ts | 32 ++ src/core/schemas/builders/lazy/lazyObject.ts | 20 + src/core/schemas/builders/list/index.ts | 1 + src/core/schemas/builders/list/list.ts | 73 +++ .../builders/literals/booleanLiteral.ts | 29 + src/core/schemas/builders/literals/index.ts | 2 + .../builders/literals/stringLiteral.ts | 29 + .../object-like/getObjectLikeUtils.ts | 79 +++ .../schemas/builders/object-like/index.ts | 2 + .../schemas/builders/object-like/types.ts | 11 + src/core/schemas/builders/object/index.ts | 22 + src/core/schemas/builders/object/object.ts | 324 +++++++++++ .../object/objectWithoutOptionalProperties.ts | 18 + src/core/schemas/builders/object/property.ts | 23 + src/core/schemas/builders/object/types.ts | 72 +++ src/core/schemas/builders/primitives/any.ts | 4 + .../schemas/builders/primitives/boolean.ts | 25 + src/core/schemas/builders/primitives/index.ts | 5 + .../schemas/builders/primitives/number.ts | 25 + .../schemas/builders/primitives/string.ts | 25 + .../schemas/builders/primitives/unknown.ts | 4 + src/core/schemas/builders/record/index.ts | 2 + src/core/schemas/builders/record/record.ts | 130 +++++ src/core/schemas/builders/record/types.ts | 17 + .../builders/schema-utils/JsonError.ts | 9 + .../builders/schema-utils/ParseError.ts | 9 + .../builders/schema-utils/getSchemaUtils.ts | 105 ++++ .../schemas/builders/schema-utils/index.ts | 4 + .../schema-utils/stringifyValidationErrors.ts | 8 + src/core/schemas/builders/set/index.ts | 1 + src/core/schemas/builders/set/set.ts | 43 ++ .../builders/undiscriminated-union/index.ts | 6 + .../builders/undiscriminated-union/types.ts | 10 + .../undiscriminatedUnion.ts | 60 ++ .../schemas/builders/union/discriminant.ts | 14 + src/core/schemas/builders/union/index.ts | 10 + src/core/schemas/builders/union/types.ts | 26 + src/core/schemas/builders/union/union.ts | 170 ++++++ src/core/schemas/index.ts | 2 + src/core/schemas/utils/MaybePromise.ts | 1 + .../addQuestionMarksToNullableProperties.ts | 15 + .../utils/createIdentitySchemaCreator.ts | 21 + src/core/schemas/utils/entries.ts | 3 + src/core/schemas/utils/filterObject.ts | 10 + .../utils/getErrorMessageForIncorrectType.ts | 25 + src/core/schemas/utils/isPlainObject.ts | 17 + src/core/schemas/utils/keys.ts | 3 + src/core/schemas/utils/maybeSkipValidation.ts | 38 ++ src/core/schemas/utils/partition.ts | 12 + src/core/websocket/events.ts | 42 ++ src/core/websocket/index.ts | 1 + src/core/websocket/ws.ts | 513 ++++++++++++++++++ src/index.ts | 2 +- src/serialization/index.ts | 1 + .../resources/apiStatus/index.ts | 1 + .../resources/apiStatus/types/ApiInfo.ts | 20 + .../resources/apiStatus/types/index.ts | 1 + .../resources/embedding/index.ts | 1 + .../resources/embedding/types/Embedding.ts | 14 + .../resources/embedding/types/index.ts | 1 + src/serialization/resources/index.ts | 10 + src/serialization/resources/tts/index.ts | 1 + .../tts/types/CancelContextRequest.ts | 23 + .../resources/tts/types/ContextId.ts | 14 + .../resources/tts/types/Controls.ts | 22 + .../resources/tts/types/Emotion.ts | 54 ++ .../resources/tts/types/GenerationRequest.ts | 40 ++ .../resources/tts/types/Mp3OutputFormat.ts | 22 + .../resources/tts/types/NaturalSpecifier.ts | 14 + .../resources/tts/types/NumericalSpecifier.ts | 16 + .../resources/tts/types/OutputFormat.ts | 38 ++ .../resources/tts/types/RawEncoding.ts | 14 + .../resources/tts/types/RawOutputFormat.ts | 23 + .../resources/tts/types/Speed.ts | 16 + .../resources/tts/types/SupportedLanguage.ts | 32 ++ .../resources/tts/types/TtsRequest.ts | 31 ++ .../tts/types/TtsRequestEmbeddingSpecifier.ts | 26 + .../tts/types/TtsRequestIdSpecifier.ts | 26 + .../tts/types/TtsRequestVoiceSpecifier.ts | 18 + .../resources/tts/types/WavOutputFormat.ts | 17 + .../tts/types/WebSocketBaseResponse.ts | 25 + .../tts/types/WebSocketChunkResponse.ts | 25 + .../tts/types/WebSocketDoneResponse.ts | 17 + .../tts/types/WebSocketErrorResponse.ts | 23 + .../tts/types/WebSocketRawOutputFormat.ts | 25 + .../resources/tts/types/WebSocketRequest.ts | 16 + .../resources/tts/types/WebSocketResponse.ts | 50 ++ .../tts/types/WebSocketStreamOptions.ts | 20 + .../tts/types/WebSocketTimestampsResponse.ts | 24 + .../resources/tts/types/WebSocketTtsOutput.ts | 26 + .../tts/types/WebSocketTtsRequest.ts | 36 ++ .../resources/tts/types/WordTimestamps.ts | 22 + .../resources/tts/types/index.ts | 30 + .../resources/voiceChanger/index.ts | 1 + .../types/OutputFormatContainer.ts | 16 + .../voiceChanger/types/StreamingResponse.ts | 40 ++ .../resources/voiceChanger/types/index.ts | 2 + .../resources/voices/client/index.ts | 1 + .../resources/voices/client/list.ts | 15 + src/serialization/resources/voices/index.ts | 2 + .../resources/voices/types/BaseVoiceId.ts | 14 + .../voices/types/CreateVoiceRequest.ts | 31 ++ .../voices/types/EmbeddingResponse.ts | 21 + .../voices/types/EmbeddingSpecifier.ts | 24 + .../resources/voices/types/Gender.ts | 16 + .../resources/voices/types/IdSpecifier.ts | 22 + .../resources/voices/types/LocalizeDialect.ts | 14 + .../voices/types/LocalizeTargetLanguage.ts | 32 ++ .../voices/types/LocalizeVoiceRequest.ts | 30 + .../voices/types/MixVoiceSpecifier.ts | 18 + .../voices/types/MixVoicesRequest.ts | 21 + .../voices/types/UpdateVoiceRequest.ts | 22 + .../resources/voices/types/Voice.ts | 37 ++ .../resources/voices/types/VoiceId.ts | 14 + .../resources/voices/types/Weight.ts | 13 + .../resources/voices/types/index.ts | 15 + src/wrapper/Client.ts | 11 + src/wrapper/StreamingTTSClient.ts | 23 + src/wrapper/Websocket.ts | 252 +++++++++ src/wrapper/source.ts | 182 +++++++ src/wrapper/utils.ts | 251 +++++++++ tests/unit/zurg/bigint/bigint.test.ts | 24 + tests/unit/zurg/date/date.test.ts | 31 ++ tests/unit/zurg/enum/enum.test.ts | 30 + tests/unit/zurg/lazy/lazy.test.ts | 57 ++ tests/unit/zurg/lazy/lazyObject.test.ts | 18 + tests/unit/zurg/lazy/recursive/a.ts | 7 + tests/unit/zurg/lazy/recursive/b.ts | 8 + tests/unit/zurg/list/list.test.ts | 41 ++ .../unit/zurg/literals/stringLiteral.test.ts | 21 + .../object-like/withParsedProperties.test.ts | 57 ++ tests/unit/zurg/object/extend.test.ts | 89 +++ tests/unit/zurg/object/object.test.ts | 255 +++++++++ .../objectWithoutOptionalProperties.test.ts | 21 + tests/unit/zurg/primitives/any.test.ts | 6 + tests/unit/zurg/primitives/boolean.test.ts | 14 + tests/unit/zurg/primitives/number.test.ts | 14 + tests/unit/zurg/primitives/string.test.ts | 14 + tests/unit/zurg/primitives/unknown.test.ts | 6 + tests/unit/zurg/record/record.test.ts | 34 ++ .../zurg/schema-utils/getSchemaUtils.test.ts | 83 +++ tests/unit/zurg/schema.test.ts | 78 +++ tests/unit/zurg/set/set.test.ts | 48 ++ tests/unit/zurg/skipValidation.test.ts | 45 ++ .../undiscriminatedUnion.test.ts | 44 ++ tests/unit/zurg/union/union.test.ts | 113 ++++ tests/unit/zurg/utils/itSchema.ts | 78 +++ tests/unit/zurg/utils/itValidate.ts | 56 ++ yarn.lock | 19 +- 162 files changed, 5748 insertions(+), 15 deletions(-) create mode 100644 src/api/resources/tts/types/WebSocketStreamOptions.ts create mode 100644 src/api/resources/tts/types/WebSocketTimestampsResponse.ts create mode 100644 src/core/schemas/Schema.ts create mode 100644 src/core/schemas/builders/bigint/bigint.ts create mode 100644 src/core/schemas/builders/bigint/index.ts create mode 100644 src/core/schemas/builders/date/date.ts create mode 100644 src/core/schemas/builders/date/index.ts create mode 100644 src/core/schemas/builders/enum/enum.ts create mode 100644 src/core/schemas/builders/enum/index.ts create mode 100644 src/core/schemas/builders/index.ts create mode 100644 src/core/schemas/builders/lazy/index.ts create mode 100644 src/core/schemas/builders/lazy/lazy.ts create mode 100644 src/core/schemas/builders/lazy/lazyObject.ts create mode 100644 src/core/schemas/builders/list/index.ts create mode 100644 src/core/schemas/builders/list/list.ts create mode 100644 src/core/schemas/builders/literals/booleanLiteral.ts create mode 100644 src/core/schemas/builders/literals/index.ts create mode 100644 src/core/schemas/builders/literals/stringLiteral.ts create mode 100644 src/core/schemas/builders/object-like/getObjectLikeUtils.ts create mode 100644 src/core/schemas/builders/object-like/index.ts create mode 100644 src/core/schemas/builders/object-like/types.ts create mode 100644 src/core/schemas/builders/object/index.ts create mode 100644 src/core/schemas/builders/object/object.ts create mode 100644 src/core/schemas/builders/object/objectWithoutOptionalProperties.ts create mode 100644 src/core/schemas/builders/object/property.ts create mode 100644 src/core/schemas/builders/object/types.ts create mode 100644 src/core/schemas/builders/primitives/any.ts create mode 100644 src/core/schemas/builders/primitives/boolean.ts create mode 100644 src/core/schemas/builders/primitives/index.ts create mode 100644 src/core/schemas/builders/primitives/number.ts create mode 100644 src/core/schemas/builders/primitives/string.ts create mode 100644 src/core/schemas/builders/primitives/unknown.ts create mode 100644 src/core/schemas/builders/record/index.ts create mode 100644 src/core/schemas/builders/record/record.ts create mode 100644 src/core/schemas/builders/record/types.ts create mode 100644 src/core/schemas/builders/schema-utils/JsonError.ts create mode 100644 src/core/schemas/builders/schema-utils/ParseError.ts create mode 100644 src/core/schemas/builders/schema-utils/getSchemaUtils.ts create mode 100644 src/core/schemas/builders/schema-utils/index.ts create mode 100644 src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts create mode 100644 src/core/schemas/builders/set/index.ts create mode 100644 src/core/schemas/builders/set/set.ts create mode 100644 src/core/schemas/builders/undiscriminated-union/index.ts create mode 100644 src/core/schemas/builders/undiscriminated-union/types.ts create mode 100644 src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts create mode 100644 src/core/schemas/builders/union/discriminant.ts create mode 100644 src/core/schemas/builders/union/index.ts create mode 100644 src/core/schemas/builders/union/types.ts create mode 100644 src/core/schemas/builders/union/union.ts create mode 100644 src/core/schemas/index.ts create mode 100644 src/core/schemas/utils/MaybePromise.ts create mode 100644 src/core/schemas/utils/addQuestionMarksToNullableProperties.ts create mode 100644 src/core/schemas/utils/createIdentitySchemaCreator.ts create mode 100644 src/core/schemas/utils/entries.ts create mode 100644 src/core/schemas/utils/filterObject.ts create mode 100644 src/core/schemas/utils/getErrorMessageForIncorrectType.ts create mode 100644 src/core/schemas/utils/isPlainObject.ts create mode 100644 src/core/schemas/utils/keys.ts create mode 100644 src/core/schemas/utils/maybeSkipValidation.ts create mode 100644 src/core/schemas/utils/partition.ts create mode 100644 src/core/websocket/events.ts create mode 100644 src/core/websocket/index.ts create mode 100644 src/core/websocket/ws.ts create mode 100644 src/serialization/index.ts create mode 100644 src/serialization/resources/apiStatus/index.ts create mode 100644 src/serialization/resources/apiStatus/types/ApiInfo.ts create mode 100644 src/serialization/resources/apiStatus/types/index.ts create mode 100644 src/serialization/resources/embedding/index.ts create mode 100644 src/serialization/resources/embedding/types/Embedding.ts create mode 100644 src/serialization/resources/embedding/types/index.ts create mode 100644 src/serialization/resources/index.ts create mode 100644 src/serialization/resources/tts/index.ts create mode 100644 src/serialization/resources/tts/types/CancelContextRequest.ts create mode 100644 src/serialization/resources/tts/types/ContextId.ts create mode 100644 src/serialization/resources/tts/types/Controls.ts create mode 100644 src/serialization/resources/tts/types/Emotion.ts create mode 100644 src/serialization/resources/tts/types/GenerationRequest.ts create mode 100644 src/serialization/resources/tts/types/Mp3OutputFormat.ts create mode 100644 src/serialization/resources/tts/types/NaturalSpecifier.ts create mode 100644 src/serialization/resources/tts/types/NumericalSpecifier.ts create mode 100644 src/serialization/resources/tts/types/OutputFormat.ts create mode 100644 src/serialization/resources/tts/types/RawEncoding.ts create mode 100644 src/serialization/resources/tts/types/RawOutputFormat.ts create mode 100644 src/serialization/resources/tts/types/Speed.ts create mode 100644 src/serialization/resources/tts/types/SupportedLanguage.ts create mode 100644 src/serialization/resources/tts/types/TtsRequest.ts create mode 100644 src/serialization/resources/tts/types/TtsRequestEmbeddingSpecifier.ts create mode 100644 src/serialization/resources/tts/types/TtsRequestIdSpecifier.ts create mode 100644 src/serialization/resources/tts/types/TtsRequestVoiceSpecifier.ts create mode 100644 src/serialization/resources/tts/types/WavOutputFormat.ts create mode 100644 src/serialization/resources/tts/types/WebSocketBaseResponse.ts create mode 100644 src/serialization/resources/tts/types/WebSocketChunkResponse.ts create mode 100644 src/serialization/resources/tts/types/WebSocketDoneResponse.ts create mode 100644 src/serialization/resources/tts/types/WebSocketErrorResponse.ts create mode 100644 src/serialization/resources/tts/types/WebSocketRawOutputFormat.ts create mode 100644 src/serialization/resources/tts/types/WebSocketRequest.ts create mode 100644 src/serialization/resources/tts/types/WebSocketResponse.ts create mode 100644 src/serialization/resources/tts/types/WebSocketStreamOptions.ts create mode 100644 src/serialization/resources/tts/types/WebSocketTimestampsResponse.ts create mode 100644 src/serialization/resources/tts/types/WebSocketTtsOutput.ts create mode 100644 src/serialization/resources/tts/types/WebSocketTtsRequest.ts create mode 100644 src/serialization/resources/tts/types/WordTimestamps.ts create mode 100644 src/serialization/resources/tts/types/index.ts create mode 100644 src/serialization/resources/voiceChanger/index.ts create mode 100644 src/serialization/resources/voiceChanger/types/OutputFormatContainer.ts create mode 100644 src/serialization/resources/voiceChanger/types/StreamingResponse.ts create mode 100644 src/serialization/resources/voiceChanger/types/index.ts create mode 100644 src/serialization/resources/voices/client/index.ts create mode 100644 src/serialization/resources/voices/client/list.ts create mode 100644 src/serialization/resources/voices/index.ts create mode 100644 src/serialization/resources/voices/types/BaseVoiceId.ts create mode 100644 src/serialization/resources/voices/types/CreateVoiceRequest.ts create mode 100644 src/serialization/resources/voices/types/EmbeddingResponse.ts create mode 100644 src/serialization/resources/voices/types/EmbeddingSpecifier.ts create mode 100644 src/serialization/resources/voices/types/Gender.ts create mode 100644 src/serialization/resources/voices/types/IdSpecifier.ts create mode 100644 src/serialization/resources/voices/types/LocalizeDialect.ts create mode 100644 src/serialization/resources/voices/types/LocalizeTargetLanguage.ts create mode 100644 src/serialization/resources/voices/types/LocalizeVoiceRequest.ts create mode 100644 src/serialization/resources/voices/types/MixVoiceSpecifier.ts create mode 100644 src/serialization/resources/voices/types/MixVoicesRequest.ts create mode 100644 src/serialization/resources/voices/types/UpdateVoiceRequest.ts create mode 100644 src/serialization/resources/voices/types/Voice.ts create mode 100644 src/serialization/resources/voices/types/VoiceId.ts create mode 100644 src/serialization/resources/voices/types/Weight.ts create mode 100644 src/serialization/resources/voices/types/index.ts create mode 100644 src/wrapper/Client.ts create mode 100644 src/wrapper/StreamingTTSClient.ts create mode 100644 src/wrapper/Websocket.ts create mode 100644 src/wrapper/source.ts create mode 100644 src/wrapper/utils.ts create mode 100644 tests/unit/zurg/bigint/bigint.test.ts create mode 100644 tests/unit/zurg/date/date.test.ts create mode 100644 tests/unit/zurg/enum/enum.test.ts create mode 100644 tests/unit/zurg/lazy/lazy.test.ts create mode 100644 tests/unit/zurg/lazy/lazyObject.test.ts create mode 100644 tests/unit/zurg/lazy/recursive/a.ts create mode 100644 tests/unit/zurg/lazy/recursive/b.ts create mode 100644 tests/unit/zurg/list/list.test.ts create mode 100644 tests/unit/zurg/literals/stringLiteral.test.ts create mode 100644 tests/unit/zurg/object-like/withParsedProperties.test.ts create mode 100644 tests/unit/zurg/object/extend.test.ts create mode 100644 tests/unit/zurg/object/object.test.ts create mode 100644 tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts create mode 100644 tests/unit/zurg/primitives/any.test.ts create mode 100644 tests/unit/zurg/primitives/boolean.test.ts create mode 100644 tests/unit/zurg/primitives/number.test.ts create mode 100644 tests/unit/zurg/primitives/string.test.ts create mode 100644 tests/unit/zurg/primitives/unknown.test.ts create mode 100644 tests/unit/zurg/record/record.test.ts create mode 100644 tests/unit/zurg/schema-utils/getSchemaUtils.test.ts create mode 100644 tests/unit/zurg/schema.test.ts create mode 100644 tests/unit/zurg/set/set.test.ts create mode 100644 tests/unit/zurg/skipValidation.test.ts create mode 100644 tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts create mode 100644 tests/unit/zurg/union/union.test.ts create mode 100644 tests/unit/zurg/utils/itSchema.ts create mode 100644 tests/unit/zurg/utils/itValidate.ts diff --git a/README.md b/README.md index 2eb2bfe..3f284c6 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,16 @@ import { CartesiaClient } from "@cartesia/cartesia-js"; const client = new CartesiaClient({ apiKeyHeader: "YOUR_API_KEY_HEADER" }); await client.tts.bytes({ - model_id: "sonic-english", + modelId: "sonic-english", transcript: "Hello, world!", voice: { mode: "id", id: "694f9389-aac1-45b6-b726-9d9369183238", }, language: "en", - output_format: { + outputFormat: { container: "raw", - sample_rate: 44100, + sampleRate: 44100, encoding: "pcm_f32le", }, }); diff --git a/package.json b/package.json index 8234ec3..be1ecf1 100644 --- a/package.json +++ b/package.json @@ -12,29 +12,33 @@ "test": "jest" }, "dependencies": { - "url-join": "4.0.1", + "emittery": "^1.0.3", "form-data": "^4.0.0", + "form-data-encoder": "^4.0.2", "formdata-node": "^6.0.3", + "human-id": "^4.1.1", "node-fetch": "2.7.0", "qs": "6.11.2", "readable-stream": "^4.5.2", - "form-data-encoder": "^4.0.2" + "url-join": "4.0.1", + "ws": "^8.14.2" }, "devDependencies": { - "@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/readable-stream": "^4.0.15", + "@types/url-join": "4.0.1", + "@types/ws": "^8.5.13", "fetch-mock-jest": "^1.5.1", - "webpack": "^5.94.0", - "ts-loader": "^9.3.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" + "ts-jest": "29.1.1", + "ts-loader": "^9.3.1", + "typescript": "4.6.4", + "webpack": "^5.94.0" }, "browser": { "fs": false, diff --git a/src/api/resources/tts/types/WebSocketStreamOptions.ts b/src/api/resources/tts/types/WebSocketStreamOptions.ts new file mode 100644 index 0000000..ff4ba05 --- /dev/null +++ b/src/api/resources/tts/types/WebSocketStreamOptions.ts @@ -0,0 +1,7 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +export interface WebSocketStreamOptions { + timeout?: number; +} diff --git a/src/api/resources/tts/types/WebSocketTimestampsResponse.ts b/src/api/resources/tts/types/WebSocketTimestampsResponse.ts new file mode 100644 index 0000000..68c368d --- /dev/null +++ b/src/api/resources/tts/types/WebSocketTimestampsResponse.ts @@ -0,0 +1,9 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as Cartesia from "../../../index"; + +export interface WebSocketTimestampsResponse extends Cartesia.WebSocketBaseResponse { + wordTimestamps?: Cartesia.WordTimestamps; +} diff --git a/src/core/schemas/Schema.ts b/src/core/schemas/Schema.ts new file mode 100644 index 0000000..2a72eac --- /dev/null +++ b/src/core/schemas/Schema.ts @@ -0,0 +1,99 @@ +import { SchemaUtils } from "./builders"; + +export type Schema = BaseSchema & SchemaUtils; + +export type inferRaw = S extends Schema ? Raw : never; +export type inferParsed = S extends Schema ? Parsed : never; + +export interface BaseSchema { + parse: (raw: unknown, opts?: SchemaOptions) => MaybeValid; + json: (parsed: unknown, opts?: SchemaOptions) => MaybeValid; + getType: () => SchemaType | SchemaType; +} + +export const SchemaType = { + BIGINT: "bigint", + DATE: "date", + ENUM: "enum", + LIST: "list", + STRING_LITERAL: "stringLiteral", + BOOLEAN_LITERAL: "booleanLiteral", + OBJECT: "object", + ANY: "any", + BOOLEAN: "boolean", + NUMBER: "number", + STRING: "string", + UNKNOWN: "unknown", + RECORD: "record", + SET: "set", + UNION: "union", + UNDISCRIMINATED_UNION: "undiscriminatedUnion", + OPTIONAL: "optional", +} as const; +export type SchemaType = typeof SchemaType[keyof typeof SchemaType]; + +export type MaybeValid = Valid | Invalid; + +export interface Valid { + ok: true; + value: T; +} + +export interface Invalid { + ok: false; + errors: ValidationError[]; +} + +export interface ValidationError { + path: string[]; + message: string; +} + +export interface SchemaOptions { + /** + * how to handle unrecognized keys in objects + * + * @default "fail" + */ + unrecognizedObjectKeys?: "fail" | "passthrough" | "strip"; + + /** + * whether to fail when an unrecognized discriminant value is + * encountered in a union + * + * @default false + */ + allowUnrecognizedUnionMembers?: boolean; + + /** + * whether to fail when an unrecognized enum value is encountered + * + * @default false + */ + allowUnrecognizedEnumValues?: boolean; + + /** + * whether to allow data that doesn't conform to the schema. + * invalid data is passed through without transformation. + * + * when this is enabled, .parse() and .json() will always + * return `ok: true`. `.parseOrThrow()` and `.jsonOrThrow()` + * will never fail. + * + * @default false + */ + skipValidation?: boolean; + + /** + * each validation failure contains a "path" property, which is + * the breadcrumbs to the offending node in the JSON. you can supply + * a prefix that is prepended to all the errors' paths. this can be + * helpful for zurg's internal debug logging. + */ + breadcrumbsPrefix?: string[]; + + /** + * whether to send 'null' for optional properties explicitly set to 'undefined'. + */ + omitUndefined?: boolean; +} diff --git a/src/core/schemas/builders/bigint/bigint.ts b/src/core/schemas/builders/bigint/bigint.ts new file mode 100644 index 0000000..dc9c742 --- /dev/null +++ b/src/core/schemas/builders/bigint/bigint.ts @@ -0,0 +1,50 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +export function bigint(): Schema { + const baseSchema: BaseSchema = { + parse: (raw, { breadcrumbsPrefix = [] } = {}) => { + if (typeof raw !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "string"), + }, + ], + }; + } + return { + ok: true, + value: BigInt(raw), + }; + }, + json: (bigint, { breadcrumbsPrefix = [] } = {}) => { + if (typeof bigint === "bigint") { + return { + ok: true, + value: bigint.toString(), + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(bigint, "bigint"), + }, + ], + }; + } + }, + getType: () => SchemaType.BIGINT, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/bigint/index.ts b/src/core/schemas/builders/bigint/index.ts new file mode 100644 index 0000000..e584304 --- /dev/null +++ b/src/core/schemas/builders/bigint/index.ts @@ -0,0 +1 @@ +export { bigint } from "./bigint"; diff --git a/src/core/schemas/builders/date/date.ts b/src/core/schemas/builders/date/date.ts new file mode 100644 index 0000000..b70f24b --- /dev/null +++ b/src/core/schemas/builders/date/date.ts @@ -0,0 +1,65 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +// https://stackoverflow.com/questions/12756159/regex-and-iso8601-formatted-datetime +const ISO_8601_REGEX = + /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; + +export function date(): Schema { + const baseSchema: BaseSchema = { + parse: (raw, { breadcrumbsPrefix = [] } = {}) => { + if (typeof raw !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "string"), + }, + ], + }; + } + if (!ISO_8601_REGEX.test(raw)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(raw, "ISO 8601 date string"), + }, + ], + }; + } + return { + ok: true, + value: new Date(raw), + }; + }, + json: (date, { breadcrumbsPrefix = [] } = {}) => { + if (date instanceof Date) { + return { + ok: true, + value: date.toISOString(), + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(date, "Date object"), + }, + ], + }; + } + }, + getType: () => SchemaType.DATE, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/date/index.ts b/src/core/schemas/builders/date/index.ts new file mode 100644 index 0000000..187b290 --- /dev/null +++ b/src/core/schemas/builders/date/index.ts @@ -0,0 +1 @@ +export { date } from "./date"; diff --git a/src/core/schemas/builders/enum/enum.ts b/src/core/schemas/builders/enum/enum.ts new file mode 100644 index 0000000..c1e24d6 --- /dev/null +++ b/src/core/schemas/builders/enum/enum.ts @@ -0,0 +1,43 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function enum_(values: E): Schema { + const validValues = new Set(values); + + const schemaCreator = createIdentitySchemaCreator( + SchemaType.ENUM, + (value, { allowUnrecognizedEnumValues, breadcrumbsPrefix = [] } = {}) => { + if (typeof value !== "string") { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "string"), + }, + ], + }; + } + + if (!validValues.has(value) && !allowUnrecognizedEnumValues) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "enum"), + }, + ], + }; + } + + return { + ok: true, + value: value as U, + }; + } + ); + + return schemaCreator(); +} diff --git a/src/core/schemas/builders/enum/index.ts b/src/core/schemas/builders/enum/index.ts new file mode 100644 index 0000000..fe6faed --- /dev/null +++ b/src/core/schemas/builders/enum/index.ts @@ -0,0 +1 @@ +export { enum_ } from "./enum"; diff --git a/src/core/schemas/builders/index.ts b/src/core/schemas/builders/index.ts new file mode 100644 index 0000000..65211f9 --- /dev/null +++ b/src/core/schemas/builders/index.ts @@ -0,0 +1,14 @@ +export * from "./bigint"; +export * from "./date"; +export * from "./enum"; +export * from "./lazy"; +export * from "./list"; +export * from "./literals"; +export * from "./object"; +export * from "./object-like"; +export * from "./primitives"; +export * from "./record"; +export * from "./schema-utils"; +export * from "./set"; +export * from "./undiscriminated-union"; +export * from "./union"; diff --git a/src/core/schemas/builders/lazy/index.ts b/src/core/schemas/builders/lazy/index.ts new file mode 100644 index 0000000..77420fb --- /dev/null +++ b/src/core/schemas/builders/lazy/index.ts @@ -0,0 +1,3 @@ +export { lazy } from "./lazy"; +export type { SchemaGetter } from "./lazy"; +export { lazyObject } from "./lazyObject"; diff --git a/src/core/schemas/builders/lazy/lazy.ts b/src/core/schemas/builders/lazy/lazy.ts new file mode 100644 index 0000000..835c61f --- /dev/null +++ b/src/core/schemas/builders/lazy/lazy.ts @@ -0,0 +1,32 @@ +import { BaseSchema, Schema } from "../../Schema"; +import { getSchemaUtils } from "../schema-utils"; + +export type SchemaGetter> = () => SchemaType; + +export function lazy(getter: SchemaGetter>): Schema { + const baseSchema = constructLazyBaseSchema(getter); + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} + +export function constructLazyBaseSchema( + getter: SchemaGetter> +): BaseSchema { + return { + parse: (raw, opts) => getMemoizedSchema(getter).parse(raw, opts), + json: (parsed, opts) => getMemoizedSchema(getter).json(parsed, opts), + getType: () => getMemoizedSchema(getter).getType(), + }; +} + +type MemoizedGetter> = SchemaGetter & { __zurg_memoized?: SchemaType }; + +export function getMemoizedSchema>(getter: SchemaGetter): SchemaType { + const castedGetter = getter as MemoizedGetter; + if (castedGetter.__zurg_memoized == null) { + castedGetter.__zurg_memoized = getter(); + } + return castedGetter.__zurg_memoized; +} diff --git a/src/core/schemas/builders/lazy/lazyObject.ts b/src/core/schemas/builders/lazy/lazyObject.ts new file mode 100644 index 0000000..38c9e28 --- /dev/null +++ b/src/core/schemas/builders/lazy/lazyObject.ts @@ -0,0 +1,20 @@ +import { getObjectUtils } from "../object"; +import { getObjectLikeUtils } from "../object-like"; +import { BaseObjectSchema, ObjectSchema } from "../object/types"; +import { getSchemaUtils } from "../schema-utils"; +import { constructLazyBaseSchema, getMemoizedSchema, SchemaGetter } from "./lazy"; + +export function lazyObject(getter: SchemaGetter>): ObjectSchema { + const baseSchema: BaseObjectSchema = { + ...constructLazyBaseSchema(getter), + _getRawProperties: () => getMemoizedSchema(getter)._getRawProperties(), + _getParsedProperties: () => getMemoizedSchema(getter)._getParsedProperties(), + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/list/index.ts b/src/core/schemas/builders/list/index.ts new file mode 100644 index 0000000..25f4bcc --- /dev/null +++ b/src/core/schemas/builders/list/index.ts @@ -0,0 +1 @@ +export { list } from "./list"; diff --git a/src/core/schemas/builders/list/list.ts b/src/core/schemas/builders/list/list.ts new file mode 100644 index 0000000..e4c5c4a --- /dev/null +++ b/src/core/schemas/builders/list/list.ts @@ -0,0 +1,73 @@ +import { BaseSchema, MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; + +export function list(schema: Schema): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => + validateAndTransformArray(raw, (item, index) => + schema.parse(item, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `[${index}]`], + }) + ), + json: (parsed, opts) => + validateAndTransformArray(parsed, (item, index) => + schema.json(item, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `[${index}]`], + }) + ), + getType: () => SchemaType.LIST, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformArray( + value: unknown, + transformItem: (item: Raw, index: number) => MaybeValid +): MaybeValid { + if (!Array.isArray(value)) { + return { + ok: false, + errors: [ + { + message: getErrorMessageForIncorrectType(value, "list"), + path: [], + }, + ], + }; + } + + const maybeValidItems = value.map((item, index) => transformItem(item, index)); + + return maybeValidItems.reduce>( + (acc, item) => { + if (acc.ok && item.ok) { + return { + ok: true, + value: [...acc.value, item.value], + }; + } + + const errors: ValidationError[] = []; + if (!acc.ok) { + errors.push(...acc.errors); + } + if (!item.ok) { + errors.push(...item.errors); + } + + return { + ok: false, + errors, + }; + }, + { ok: true, value: [] } + ); +} diff --git a/src/core/schemas/builders/literals/booleanLiteral.ts b/src/core/schemas/builders/literals/booleanLiteral.ts new file mode 100644 index 0000000..a83d22c --- /dev/null +++ b/src/core/schemas/builders/literals/booleanLiteral.ts @@ -0,0 +1,29 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function booleanLiteral(literal: V): Schema { + const schemaCreator = createIdentitySchemaCreator( + SchemaType.BOOLEAN_LITERAL, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (value === literal) { + return { + ok: true, + value: literal, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, `${literal.toString()}`), + }, + ], + }; + } + } + ); + + return schemaCreator(); +} diff --git a/src/core/schemas/builders/literals/index.ts b/src/core/schemas/builders/literals/index.ts new file mode 100644 index 0000000..d2bf08f --- /dev/null +++ b/src/core/schemas/builders/literals/index.ts @@ -0,0 +1,2 @@ +export { stringLiteral } from "./stringLiteral"; +export { booleanLiteral } from "./booleanLiteral"; diff --git a/src/core/schemas/builders/literals/stringLiteral.ts b/src/core/schemas/builders/literals/stringLiteral.ts new file mode 100644 index 0000000..3939b76 --- /dev/null +++ b/src/core/schemas/builders/literals/stringLiteral.ts @@ -0,0 +1,29 @@ +import { Schema, SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export function stringLiteral(literal: V): Schema { + const schemaCreator = createIdentitySchemaCreator( + SchemaType.STRING_LITERAL, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (value === literal) { + return { + ok: true, + value: literal, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, `"${literal}"`), + }, + ], + }; + } + } + ); + + return schemaCreator(); +} diff --git a/src/core/schemas/builders/object-like/getObjectLikeUtils.ts b/src/core/schemas/builders/object-like/getObjectLikeUtils.ts new file mode 100644 index 0000000..8331d08 --- /dev/null +++ b/src/core/schemas/builders/object-like/getObjectLikeUtils.ts @@ -0,0 +1,79 @@ +import { BaseSchema } from "../../Schema"; +import { filterObject } from "../../utils/filterObject"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { getSchemaUtils } from "../schema-utils"; +import { ObjectLikeSchema, ObjectLikeUtils } from "./types"; + +export function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils { + return { + withParsedProperties: (properties) => withParsedProperties(schema, properties), + }; +} + +/** + * object-like utils are defined in one file to resolve issues with circular imports + */ + +export function withParsedProperties( + objectLike: BaseSchema, + properties: { [K in keyof Properties]: Properties[K] | ((parsed: ParsedObjectShape) => Properties[K]) } +): ObjectLikeSchema { + const objectSchema: BaseSchema = { + parse: (raw, opts) => { + const parsedObject = objectLike.parse(raw, opts); + if (!parsedObject.ok) { + return parsedObject; + } + + const additionalProperties = Object.entries(properties).reduce>( + (processed, [key, value]) => { + return { + ...processed, + [key]: typeof value === "function" ? value(parsedObject.value) : value, + }; + }, + {} + ); + + return { + ok: true, + value: { + ...parsedObject.value, + ...(additionalProperties as Properties), + }, + }; + }, + + json: (parsed, opts) => { + if (!isPlainObject(parsed)) { + return { + ok: false, + errors: [ + { + path: opts?.breadcrumbsPrefix ?? [], + message: getErrorMessageForIncorrectType(parsed, "object"), + }, + ], + }; + } + + // strip out added properties + const addedPropertyKeys = new Set(Object.keys(properties)); + const parsedWithoutAddedProperties = filterObject( + parsed, + Object.keys(parsed).filter((key) => !addedPropertyKeys.has(key)) + ); + + return objectLike.json(parsedWithoutAddedProperties as ParsedObjectShape, opts); + }, + + getType: () => objectLike.getType(), + }; + + return { + ...objectSchema, + ...getSchemaUtils(objectSchema), + ...getObjectLikeUtils(objectSchema), + }; +} diff --git a/src/core/schemas/builders/object-like/index.ts b/src/core/schemas/builders/object-like/index.ts new file mode 100644 index 0000000..c342e72 --- /dev/null +++ b/src/core/schemas/builders/object-like/index.ts @@ -0,0 +1,2 @@ +export { getObjectLikeUtils, withParsedProperties } from "./getObjectLikeUtils"; +export type { ObjectLikeSchema, ObjectLikeUtils } from "./types"; diff --git a/src/core/schemas/builders/object-like/types.ts b/src/core/schemas/builders/object-like/types.ts new file mode 100644 index 0000000..75b3698 --- /dev/null +++ b/src/core/schemas/builders/object-like/types.ts @@ -0,0 +1,11 @@ +import { BaseSchema, Schema } from "../../Schema"; + +export type ObjectLikeSchema = Schema & + BaseSchema & + ObjectLikeUtils; + +export interface ObjectLikeUtils { + withParsedProperties: >(properties: { + [K in keyof T]: T[K] | ((parsed: Parsed) => T[K]); + }) => ObjectLikeSchema; +} diff --git a/src/core/schemas/builders/object/index.ts b/src/core/schemas/builders/object/index.ts new file mode 100644 index 0000000..e3f4388 --- /dev/null +++ b/src/core/schemas/builders/object/index.ts @@ -0,0 +1,22 @@ +export { getObjectUtils, object } from "./object"; +export { objectWithoutOptionalProperties } from "./objectWithoutOptionalProperties"; +export type { + inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas, + inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas, +} from "./objectWithoutOptionalProperties"; +export { isProperty, property } from "./property"; +export type { Property } from "./property"; +export type { + BaseObjectSchema, + inferObjectSchemaFromPropertySchemas, + inferParsedObject, + inferParsedObjectFromPropertySchemas, + inferParsedPropertySchema, + inferRawKey, + inferRawObject, + inferRawObjectFromPropertySchemas, + inferRawPropertySchema, + ObjectSchema, + ObjectUtils, + PropertySchemas, +} from "./types"; diff --git a/src/core/schemas/builders/object/object.ts b/src/core/schemas/builders/object/object.ts new file mode 100644 index 0000000..e00136d --- /dev/null +++ b/src/core/schemas/builders/object/object.ts @@ -0,0 +1,324 @@ +import { MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { entries } from "../../utils/entries"; +import { filterObject } from "../../utils/filterObject"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { keys } from "../../utils/keys"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { partition } from "../../utils/partition"; +import { getObjectLikeUtils } from "../object-like"; +import { getSchemaUtils } from "../schema-utils"; +import { isProperty } from "./property"; +import { + BaseObjectSchema, + inferObjectSchemaFromPropertySchemas, + inferParsedObjectFromPropertySchemas, + inferRawObjectFromPropertySchemas, + ObjectSchema, + ObjectUtils, + PropertySchemas, +} from "./types"; + +interface ObjectPropertyWithRawKey { + rawKey: string; + parsedKey: string; + valueSchema: Schema; +} + +export function object>( + schemas: T +): inferObjectSchemaFromPropertySchemas { + const baseSchema: BaseObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas + > = { + _getRawProperties: () => + Object.entries(schemas).map(([parsedKey, propertySchema]) => + isProperty(propertySchema) ? propertySchema.rawKey : parsedKey + ) as unknown as (keyof inferRawObjectFromPropertySchemas)[], + _getParsedProperties: () => keys(schemas) as unknown as (keyof inferParsedObjectFromPropertySchemas)[], + + parse: (raw, opts) => { + const rawKeyToProperty: Record = {}; + const requiredKeys: string[] = []; + + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; + + const property: ObjectPropertyWithRawKey = { + rawKey, + parsedKey: parsedKey as string, + valueSchema, + }; + + rawKeyToProperty[rawKey] = property; + + if (isSchemaRequired(valueSchema)) { + requiredKeys.push(rawKey); + } + } + + return validateAndTransformObject({ + value: raw, + requiredKeys, + getProperty: (rawKey) => { + const property = rawKeyToProperty[rawKey]; + if (property == null) { + return undefined; + } + return { + transformedKey: property.parsedKey, + transform: (propertyValue) => + property.valueSchema.parse(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawKey], + }), + }; + }, + unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, + skipValidation: opts?.skipValidation, + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + omitUndefined: opts?.omitUndefined, + }); + }, + + json: (parsed, opts) => { + const requiredKeys: string[] = []; + + for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { + const valueSchema: Schema = isProperty(schemaOrObjectProperty) + ? schemaOrObjectProperty.valueSchema + : schemaOrObjectProperty; + + if (isSchemaRequired(valueSchema)) { + requiredKeys.push(parsedKey as string); + } + } + + return validateAndTransformObject({ + value: parsed, + requiredKeys, + getProperty: ( + parsedKey + ): { transformedKey: string; transform: (propertyValue: unknown) => MaybeValid } | undefined => { + const property = schemas[parsedKey as keyof T]; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (property == null) { + return undefined; + } + + if (isProperty(property)) { + return { + transformedKey: property.rawKey, + transform: (propertyValue) => + property.valueSchema.json(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], + }), + }; + } else { + return { + transformedKey: parsedKey, + transform: (propertyValue) => + property.json(propertyValue, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedKey], + }), + }; + } + }, + unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, + skipValidation: opts?.skipValidation, + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + omitUndefined: opts?.omitUndefined, + }); + }, + + getType: () => SchemaType.OBJECT, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; +} + +function validateAndTransformObject({ + value, + requiredKeys, + getProperty, + unrecognizedObjectKeys = "fail", + skipValidation = false, + breadcrumbsPrefix = [], +}: { + value: unknown; + requiredKeys: string[]; + getProperty: ( + preTransformedKey: string + ) => { transformedKey: string; transform: (propertyValue: unknown) => MaybeValid } | undefined; + unrecognizedObjectKeys: "fail" | "passthrough" | "strip" | undefined; + skipValidation: boolean | undefined; + breadcrumbsPrefix: string[] | undefined; + omitUndefined: boolean | undefined; +}): MaybeValid { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + const missingRequiredKeys = new Set(requiredKeys); + const errors: ValidationError[] = []; + const transformed: Record = {}; + + for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { + const property = getProperty(preTransformedKey); + + if (property != null) { + missingRequiredKeys.delete(preTransformedKey); + + const value = property.transform(preTransformedItemValue); + if (value.ok) { + transformed[property.transformedKey] = value.value; + } else { + transformed[preTransformedKey] = preTransformedItemValue; + errors.push(...value.errors); + } + } else { + switch (unrecognizedObjectKeys) { + case "fail": + errors.push({ + path: [...breadcrumbsPrefix, preTransformedKey], + message: `Unexpected key "${preTransformedKey}"`, + }); + break; + case "strip": + break; + case "passthrough": + transformed[preTransformedKey] = preTransformedItemValue; + break; + } + } + } + + errors.push( + ...requiredKeys + .filter((key) => missingRequiredKeys.has(key)) + .map((key) => ({ + path: breadcrumbsPrefix, + message: `Missing required key "${key}"`, + })) + ); + + if (errors.length === 0 || skipValidation) { + return { + ok: true, + value: transformed as Transformed, + }; + } else { + return { + ok: false, + errors, + }; + } +} + +export function getObjectUtils(schema: BaseObjectSchema): ObjectUtils { + return { + extend: (extension: ObjectSchema) => { + const baseSchema: BaseObjectSchema = { + _getParsedProperties: () => [...schema._getParsedProperties(), ...extension._getParsedProperties()], + _getRawProperties: () => [...schema._getRawProperties(), ...extension._getRawProperties()], + parse: (raw, opts) => { + return validateAndTransformExtendedObject({ + extensionKeys: extension._getRawProperties(), + value: raw, + transformBase: (rawBase) => schema.parse(rawBase, opts), + transformExtension: (rawExtension) => extension.parse(rawExtension, opts), + }); + }, + json: (parsed, opts) => { + return validateAndTransformExtendedObject({ + extensionKeys: extension._getParsedProperties(), + value: parsed, + transformBase: (parsedBase) => schema.json(parsedBase, opts), + transformExtension: (parsedExtension) => extension.json(parsedExtension, opts), + }); + }, + getType: () => SchemaType.OBJECT, + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + ...getObjectUtils(baseSchema), + }; + }, + }; +} + +function validateAndTransformExtendedObject({ + extensionKeys, + value, + transformBase, + transformExtension, +}: { + extensionKeys: (keyof PreTransformedExtension)[]; + value: unknown; + transformBase: (value: unknown) => MaybeValid; + transformExtension: (value: unknown) => MaybeValid; +}): MaybeValid { + const extensionPropertiesSet = new Set(extensionKeys); + const [extensionProperties, baseProperties] = partition(keys(value), (key) => + extensionPropertiesSet.has(key as keyof PreTransformedExtension) + ); + + const transformedBase = transformBase(filterObject(value, baseProperties)); + const transformedExtension = transformExtension(filterObject(value, extensionProperties)); + + if (transformedBase.ok && transformedExtension.ok) { + return { + ok: true, + value: { + ...transformedBase.value, + ...transformedExtension.value, + }, + }; + } else { + return { + ok: false, + errors: [ + ...(transformedBase.ok ? [] : transformedBase.errors), + ...(transformedExtension.ok ? [] : transformedExtension.errors), + ], + }; + } +} + +function isSchemaRequired(schema: Schema): boolean { + return !isSchemaOptional(schema); +} + +function isSchemaOptional(schema: Schema): boolean { + switch (schema.getType()) { + case SchemaType.ANY: + case SchemaType.UNKNOWN: + case SchemaType.OPTIONAL: + return true; + default: + return false; + } +} diff --git a/src/core/schemas/builders/object/objectWithoutOptionalProperties.ts b/src/core/schemas/builders/object/objectWithoutOptionalProperties.ts new file mode 100644 index 0000000..a0951f4 --- /dev/null +++ b/src/core/schemas/builders/object/objectWithoutOptionalProperties.ts @@ -0,0 +1,18 @@ +import { object } from "./object"; +import { inferParsedPropertySchema, inferRawObjectFromPropertySchemas, ObjectSchema, PropertySchemas } from "./types"; + +export function objectWithoutOptionalProperties>( + schemas: T +): inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas { + return object(schemas) as unknown as inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas; +} + +export type inferObjectWithoutOptionalPropertiesSchemaFromPropertySchemas> = + ObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas + >; + +export type inferParsedObjectWithoutOptionalPropertiesFromPropertySchemas> = { + [K in keyof T]: inferParsedPropertySchema; +}; diff --git a/src/core/schemas/builders/object/property.ts b/src/core/schemas/builders/object/property.ts new file mode 100644 index 0000000..d245c4b --- /dev/null +++ b/src/core/schemas/builders/object/property.ts @@ -0,0 +1,23 @@ +import { Schema } from "../../Schema"; + +export function property( + rawKey: RawKey, + valueSchema: Schema +): Property { + return { + rawKey, + valueSchema, + isProperty: true, + }; +} + +export interface Property { + rawKey: RawKey; + valueSchema: Schema; + isProperty: true; +} + +export function isProperty>(maybeProperty: unknown): maybeProperty is O { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return (maybeProperty as O).isProperty; +} diff --git a/src/core/schemas/builders/object/types.ts b/src/core/schemas/builders/object/types.ts new file mode 100644 index 0000000..de9bb40 --- /dev/null +++ b/src/core/schemas/builders/object/types.ts @@ -0,0 +1,72 @@ +import { BaseSchema, inferParsed, inferRaw, Schema } from "../../Schema"; +import { addQuestionMarksToNullableProperties } from "../../utils/addQuestionMarksToNullableProperties"; +import { ObjectLikeUtils } from "../object-like"; +import { SchemaUtils } from "../schema-utils"; +import { Property } from "./property"; + +export type ObjectSchema = BaseObjectSchema & + ObjectLikeUtils & + ObjectUtils & + SchemaUtils; + +export interface BaseObjectSchema extends BaseSchema { + _getRawProperties: () => (keyof Raw)[]; + _getParsedProperties: () => (keyof Parsed)[]; +} + +export interface ObjectUtils { + extend: ( + schemas: ObjectSchema + ) => ObjectSchema; +} + +export type inferRawObject> = O extends ObjectSchema ? Raw : never; + +export type inferParsedObject> = O extends ObjectSchema + ? Parsed + : never; + +export type inferObjectSchemaFromPropertySchemas> = ObjectSchema< + inferRawObjectFromPropertySchemas, + inferParsedObjectFromPropertySchemas +>; + +export type inferRawObjectFromPropertySchemas> = + addQuestionMarksToNullableProperties<{ + [ParsedKey in keyof T as inferRawKey]: inferRawPropertySchema; + }>; + +export type inferParsedObjectFromPropertySchemas> = + addQuestionMarksToNullableProperties<{ + [K in keyof T]: inferParsedPropertySchema; + }>; + +export type PropertySchemas = Record< + ParsedKeys, + Property | Schema +>; + +export type inferRawPropertySchema

| Schema> = P extends Property< + any, + infer Raw, + any +> + ? Raw + : P extends Schema + ? inferRaw

+ : never; + +export type inferParsedPropertySchema

| Schema> = P extends Property< + any, + any, + infer Parsed +> + ? Parsed + : P extends Schema + ? inferParsed

+ : never; + +export type inferRawKey< + ParsedKey extends string | number | symbol, + P extends Property | Schema +> = P extends Property ? Raw : ParsedKey; diff --git a/src/core/schemas/builders/primitives/any.ts b/src/core/schemas/builders/primitives/any.ts new file mode 100644 index 0000000..fcaeb04 --- /dev/null +++ b/src/core/schemas/builders/primitives/any.ts @@ -0,0 +1,4 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; + +export const any = createIdentitySchemaCreator(SchemaType.ANY, (value) => ({ ok: true, value })); diff --git a/src/core/schemas/builders/primitives/boolean.ts b/src/core/schemas/builders/primitives/boolean.ts new file mode 100644 index 0000000..fad6056 --- /dev/null +++ b/src/core/schemas/builders/primitives/boolean.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const boolean = createIdentitySchemaCreator( + SchemaType.BOOLEAN, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "boolean") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "boolean"), + }, + ], + }; + } + } +); diff --git a/src/core/schemas/builders/primitives/index.ts b/src/core/schemas/builders/primitives/index.ts new file mode 100644 index 0000000..788f941 --- /dev/null +++ b/src/core/schemas/builders/primitives/index.ts @@ -0,0 +1,5 @@ +export { any } from "./any"; +export { boolean } from "./boolean"; +export { number } from "./number"; +export { string } from "./string"; +export { unknown } from "./unknown"; diff --git a/src/core/schemas/builders/primitives/number.ts b/src/core/schemas/builders/primitives/number.ts new file mode 100644 index 0000000..c268945 --- /dev/null +++ b/src/core/schemas/builders/primitives/number.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const number = createIdentitySchemaCreator( + SchemaType.NUMBER, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "number") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "number"), + }, + ], + }; + } + } +); diff --git a/src/core/schemas/builders/primitives/string.ts b/src/core/schemas/builders/primitives/string.ts new file mode 100644 index 0000000..949f1f2 --- /dev/null +++ b/src/core/schemas/builders/primitives/string.ts @@ -0,0 +1,25 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; + +export const string = createIdentitySchemaCreator( + SchemaType.STRING, + (value, { breadcrumbsPrefix = [] } = {}) => { + if (typeof value === "string") { + return { + ok: true, + value, + }; + } else { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "string"), + }, + ], + }; + } + } +); diff --git a/src/core/schemas/builders/primitives/unknown.ts b/src/core/schemas/builders/primitives/unknown.ts new file mode 100644 index 0000000..4d52495 --- /dev/null +++ b/src/core/schemas/builders/primitives/unknown.ts @@ -0,0 +1,4 @@ +import { SchemaType } from "../../Schema"; +import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; + +export const unknown = createIdentitySchemaCreator(SchemaType.UNKNOWN, (value) => ({ ok: true, value })); diff --git a/src/core/schemas/builders/record/index.ts b/src/core/schemas/builders/record/index.ts new file mode 100644 index 0000000..82e25c5 --- /dev/null +++ b/src/core/schemas/builders/record/index.ts @@ -0,0 +1,2 @@ +export { record } from "./record"; +export type { BaseRecordSchema, RecordSchema } from "./types"; diff --git a/src/core/schemas/builders/record/record.ts b/src/core/schemas/builders/record/record.ts new file mode 100644 index 0000000..6683ac3 --- /dev/null +++ b/src/core/schemas/builders/record/record.ts @@ -0,0 +1,130 @@ +import { MaybeValid, Schema, SchemaType, ValidationError } from "../../Schema"; +import { entries } from "../../utils/entries"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; +import { BaseRecordSchema, RecordSchema } from "./types"; + +export function record( + keySchema: Schema, + valueSchema: Schema +): RecordSchema { + const baseSchema: BaseRecordSchema = { + parse: (raw, opts) => { + return validateAndTransformRecord({ + value: raw, + isKeyNumeric: keySchema.getType() === SchemaType.NUMBER, + transformKey: (key) => + keySchema.parse(key, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key} (key)`], + }), + transformValue: (value, key) => + valueSchema.parse(value, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key}`], + }), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + json: (parsed, opts) => { + return validateAndTransformRecord({ + value: parsed, + isKeyNumeric: keySchema.getType() === SchemaType.NUMBER, + transformKey: (key) => + keySchema.json(key, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key} (key)`], + }), + transformValue: (value, key) => + valueSchema.json(value, { + ...opts, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), `${key}`], + }), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + getType: () => SchemaType.RECORD, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformRecord({ + value, + isKeyNumeric, + transformKey, + transformValue, + breadcrumbsPrefix = [], +}: { + value: unknown; + isKeyNumeric: boolean; + transformKey: (key: string | number) => MaybeValid; + transformValue: (value: unknown, key: string | number) => MaybeValid; + breadcrumbsPrefix: string[] | undefined; +}): MaybeValid> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + return entries(value).reduce>>( + (accPromise, [stringKey, value]) => { + // skip nullish keys + if (value == null) { + return accPromise; + } + + const acc = accPromise; + + let key: string | number = stringKey; + if (isKeyNumeric) { + const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; + if (!isNaN(numberKey)) { + key = numberKey; + } + } + const transformedKey = transformKey(key); + + const transformedValue = transformValue(value, key); + + if (acc.ok && transformedKey.ok && transformedValue.ok) { + return { + ok: true, + value: { + ...acc.value, + [transformedKey.value]: transformedValue.value, + }, + }; + } + + const errors: ValidationError[] = []; + if (!acc.ok) { + errors.push(...acc.errors); + } + if (!transformedKey.ok) { + errors.push(...transformedKey.errors); + } + if (!transformedValue.ok) { + errors.push(...transformedValue.errors); + } + + return { + ok: false, + errors, + }; + }, + { ok: true, value: {} as Record } + ); +} diff --git a/src/core/schemas/builders/record/types.ts b/src/core/schemas/builders/record/types.ts new file mode 100644 index 0000000..eb82cc7 --- /dev/null +++ b/src/core/schemas/builders/record/types.ts @@ -0,0 +1,17 @@ +import { BaseSchema } from "../../Schema"; +import { SchemaUtils } from "../schema-utils"; + +export type RecordSchema< + RawKey extends string | number, + RawValue, + ParsedKey extends string | number, + ParsedValue +> = BaseRecordSchema & + SchemaUtils, Record>; + +export type BaseRecordSchema< + RawKey extends string | number, + RawValue, + ParsedKey extends string | number, + ParsedValue +> = BaseSchema, Record>; diff --git a/src/core/schemas/builders/schema-utils/JsonError.ts b/src/core/schemas/builders/schema-utils/JsonError.ts new file mode 100644 index 0000000..2b89ca0 --- /dev/null +++ b/src/core/schemas/builders/schema-utils/JsonError.ts @@ -0,0 +1,9 @@ +import { ValidationError } from "../../Schema"; +import { stringifyValidationError } from "./stringifyValidationErrors"; + +export class JsonError extends Error { + constructor(public readonly errors: ValidationError[]) { + super(errors.map(stringifyValidationError).join("; ")); + Object.setPrototypeOf(this, JsonError.prototype); + } +} diff --git a/src/core/schemas/builders/schema-utils/ParseError.ts b/src/core/schemas/builders/schema-utils/ParseError.ts new file mode 100644 index 0000000..d056eb4 --- /dev/null +++ b/src/core/schemas/builders/schema-utils/ParseError.ts @@ -0,0 +1,9 @@ +import { ValidationError } from "../../Schema"; +import { stringifyValidationError } from "./stringifyValidationErrors"; + +export class ParseError extends Error { + constructor(public readonly errors: ValidationError[]) { + super(errors.map(stringifyValidationError).join("; ")); + Object.setPrototypeOf(this, ParseError.prototype); + } +} diff --git a/src/core/schemas/builders/schema-utils/getSchemaUtils.ts b/src/core/schemas/builders/schema-utils/getSchemaUtils.ts new file mode 100644 index 0000000..79ecad9 --- /dev/null +++ b/src/core/schemas/builders/schema-utils/getSchemaUtils.ts @@ -0,0 +1,105 @@ +import { BaseSchema, Schema, SchemaOptions, SchemaType } from "../../Schema"; +import { JsonError } from "./JsonError"; +import { ParseError } from "./ParseError"; + +export interface SchemaUtils { + optional: () => Schema; + transform: (transformer: SchemaTransformer) => Schema; + parseOrThrow: (raw: unknown, opts?: SchemaOptions) => Parsed; + jsonOrThrow: (raw: unknown, opts?: SchemaOptions) => Raw; +} + +export interface SchemaTransformer { + transform: (parsed: Parsed) => Transformed; + untransform: (transformed: any) => Parsed; +} + +export function getSchemaUtils(schema: BaseSchema): SchemaUtils { + return { + optional: () => optional(schema), + transform: (transformer) => transform(schema, transformer), + parseOrThrow: (raw, opts) => { + const parsed = schema.parse(raw, opts); + if (parsed.ok) { + return parsed.value; + } + throw new ParseError(parsed.errors); + }, + jsonOrThrow: (parsed, opts) => { + const raw = schema.json(parsed, opts); + if (raw.ok) { + return raw.value; + } + throw new JsonError(raw.errors); + }, + }; +} + +/** + * schema utils are defined in one file to resolve issues with circular imports + */ + +export function optional( + schema: BaseSchema +): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => { + if (raw == null) { + return { + ok: true, + value: undefined, + }; + } + return schema.parse(raw, opts); + }, + json: (parsed, opts) => { + if (opts?.omitUndefined && parsed === undefined) { + return { + ok: true, + value: undefined, + }; + } + if (parsed == null) { + return { + ok: true, + value: null, + }; + } + return schema.json(parsed, opts); + }, + getType: () => SchemaType.OPTIONAL, + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} + +export function transform( + schema: BaseSchema, + transformer: SchemaTransformer +): Schema { + const baseSchema: BaseSchema = { + parse: (raw, opts) => { + const parsed = schema.parse(raw, opts); + if (!parsed.ok) { + return parsed; + } + return { + ok: true, + value: transformer.transform(parsed.value), + }; + }, + json: (transformed, opts) => { + const parsed = transformer.untransform(transformed); + return schema.json(parsed, opts); + }, + getType: () => schema.getType(), + }; + + return { + ...baseSchema, + ...getSchemaUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/schema-utils/index.ts b/src/core/schemas/builders/schema-utils/index.ts new file mode 100644 index 0000000..aa04e05 --- /dev/null +++ b/src/core/schemas/builders/schema-utils/index.ts @@ -0,0 +1,4 @@ +export { getSchemaUtils, optional, transform } from "./getSchemaUtils"; +export type { SchemaUtils } from "./getSchemaUtils"; +export { JsonError } from "./JsonError"; +export { ParseError } from "./ParseError"; diff --git a/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts b/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts new file mode 100644 index 0000000..4160f0a --- /dev/null +++ b/src/core/schemas/builders/schema-utils/stringifyValidationErrors.ts @@ -0,0 +1,8 @@ +import { ValidationError } from "../../Schema"; + +export function stringifyValidationError(error: ValidationError): string { + if (error.path.length === 0) { + return error.message; + } + return `${error.path.join(" -> ")}: ${error.message}`; +} diff --git a/src/core/schemas/builders/set/index.ts b/src/core/schemas/builders/set/index.ts new file mode 100644 index 0000000..f3310e8 --- /dev/null +++ b/src/core/schemas/builders/set/index.ts @@ -0,0 +1 @@ +export { set } from "./set"; diff --git a/src/core/schemas/builders/set/set.ts b/src/core/schemas/builders/set/set.ts new file mode 100644 index 0000000..e9e6bb7 --- /dev/null +++ b/src/core/schemas/builders/set/set.ts @@ -0,0 +1,43 @@ +import { BaseSchema, Schema, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { list } from "../list"; +import { getSchemaUtils } from "../schema-utils"; + +export function set(schema: Schema): Schema> { + const listSchema = list(schema); + const baseSchema: BaseSchema> = { + parse: (raw, opts) => { + const parsedList = listSchema.parse(raw, opts); + if (parsedList.ok) { + return { + ok: true, + value: new Set(parsedList.value), + }; + } else { + return parsedList; + } + }, + json: (parsed, opts) => { + if (!(parsed instanceof Set)) { + return { + ok: false, + errors: [ + { + path: opts?.breadcrumbsPrefix ?? [], + message: getErrorMessageForIncorrectType(parsed, "Set"), + }, + ], + }; + } + const jsonList = listSchema.json([...parsed], opts); + return jsonList; + }, + getType: () => SchemaType.SET, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} diff --git a/src/core/schemas/builders/undiscriminated-union/index.ts b/src/core/schemas/builders/undiscriminated-union/index.ts new file mode 100644 index 0000000..75b71cb --- /dev/null +++ b/src/core/schemas/builders/undiscriminated-union/index.ts @@ -0,0 +1,6 @@ +export type { + inferParsedUnidiscriminatedUnionSchema, + inferRawUnidiscriminatedUnionSchema, + UndiscriminatedUnionSchema, +} from "./types"; +export { undiscriminatedUnion } from "./undiscriminatedUnion"; diff --git a/src/core/schemas/builders/undiscriminated-union/types.ts b/src/core/schemas/builders/undiscriminated-union/types.ts new file mode 100644 index 0000000..43e7108 --- /dev/null +++ b/src/core/schemas/builders/undiscriminated-union/types.ts @@ -0,0 +1,10 @@ +import { inferParsed, inferRaw, Schema } from "../../Schema"; + +export type UndiscriminatedUnionSchema = Schema< + inferRawUnidiscriminatedUnionSchema, + inferParsedUnidiscriminatedUnionSchema +>; + +export type inferRawUnidiscriminatedUnionSchema = inferRaw; + +export type inferParsedUnidiscriminatedUnionSchema = inferParsed; diff --git a/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts b/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts new file mode 100644 index 0000000..21ed3df --- /dev/null +++ b/src/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.ts @@ -0,0 +1,60 @@ +import { BaseSchema, MaybeValid, Schema, SchemaOptions, SchemaType, ValidationError } from "../../Schema"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { getSchemaUtils } from "../schema-utils"; +import { inferParsedUnidiscriminatedUnionSchema, inferRawUnidiscriminatedUnionSchema } from "./types"; + +export function undiscriminatedUnion, ...Schema[]]>( + schemas: Schemas +): Schema, inferParsedUnidiscriminatedUnionSchema> { + const baseSchema: BaseSchema< + inferRawUnidiscriminatedUnionSchema, + inferParsedUnidiscriminatedUnionSchema + > = { + parse: (raw, opts) => { + return validateAndTransformUndiscriminatedUnion>( + (schema, opts) => schema.parse(raw, opts), + schemas, + opts + ); + }, + json: (parsed, opts) => { + return validateAndTransformUndiscriminatedUnion>( + (schema, opts) => schema.json(parsed, opts), + schemas, + opts + ); + }, + getType: () => SchemaType.UNDISCRIMINATED_UNION, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; +} + +function validateAndTransformUndiscriminatedUnion( + transform: (schema: Schema, opts: SchemaOptions) => MaybeValid, + schemas: Schema[], + opts: SchemaOptions | undefined +): MaybeValid { + const errors: ValidationError[] = []; + for (const [index, schema] of schemas.entries()) { + const transformed = transform(schema, { ...opts, skipValidation: false }); + if (transformed.ok) { + return transformed; + } else { + for (const error of transformed.errors) { + errors.push({ + path: error.path, + message: `[Variant ${index}] ${error.message}`, + }); + } + } + } + + return { + ok: false, + errors, + }; +} diff --git a/src/core/schemas/builders/union/discriminant.ts b/src/core/schemas/builders/union/discriminant.ts new file mode 100644 index 0000000..55065bc --- /dev/null +++ b/src/core/schemas/builders/union/discriminant.ts @@ -0,0 +1,14 @@ +export function discriminant( + parsedDiscriminant: ParsedDiscriminant, + rawDiscriminant: RawDiscriminant +): Discriminant { + return { + parsedDiscriminant, + rawDiscriminant, + }; +} + +export interface Discriminant { + parsedDiscriminant: ParsedDiscriminant; + rawDiscriminant: RawDiscriminant; +} diff --git a/src/core/schemas/builders/union/index.ts b/src/core/schemas/builders/union/index.ts new file mode 100644 index 0000000..85fc008 --- /dev/null +++ b/src/core/schemas/builders/union/index.ts @@ -0,0 +1,10 @@ +export { discriminant } from "./discriminant"; +export type { Discriminant } from "./discriminant"; +export type { + inferParsedDiscriminant, + inferParsedUnion, + inferRawDiscriminant, + inferRawUnion, + UnionSubtypes, +} from "./types"; +export { union } from "./union"; diff --git a/src/core/schemas/builders/union/types.ts b/src/core/schemas/builders/union/types.ts new file mode 100644 index 0000000..6f82c86 --- /dev/null +++ b/src/core/schemas/builders/union/types.ts @@ -0,0 +1,26 @@ +import { inferParsedObject, inferRawObject, ObjectSchema } from "../object"; +import { Discriminant } from "./discriminant"; + +export type UnionSubtypes = { + [K in DiscriminantValues]: ObjectSchema; +}; + +export type inferRawUnion, U extends UnionSubtypes> = { + [K in keyof U]: Record, K> & inferRawObject; +}[keyof U]; + +export type inferParsedUnion, U extends UnionSubtypes> = { + [K in keyof U]: Record, K> & inferParsedObject; +}[keyof U]; + +export type inferRawDiscriminant> = D extends string + ? D + : D extends Discriminant + ? Raw + : never; + +export type inferParsedDiscriminant> = D extends string + ? D + : D extends Discriminant + ? Parsed + : never; diff --git a/src/core/schemas/builders/union/union.ts b/src/core/schemas/builders/union/union.ts new file mode 100644 index 0000000..ab61475 --- /dev/null +++ b/src/core/schemas/builders/union/union.ts @@ -0,0 +1,170 @@ +import { BaseSchema, MaybeValid, SchemaType } from "../../Schema"; +import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; +import { isPlainObject } from "../../utils/isPlainObject"; +import { keys } from "../../utils/keys"; +import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; +import { enum_ } from "../enum"; +import { ObjectSchema } from "../object"; +import { getObjectLikeUtils, ObjectLikeSchema } from "../object-like"; +import { getSchemaUtils } from "../schema-utils"; +import { Discriminant } from "./discriminant"; +import { inferParsedDiscriminant, inferParsedUnion, inferRawDiscriminant, inferRawUnion, UnionSubtypes } from "./types"; + +export function union, U extends UnionSubtypes>( + discriminant: D, + union: U +): ObjectLikeSchema, inferParsedUnion> { + const rawDiscriminant = + typeof discriminant === "string" ? discriminant : (discriminant.rawDiscriminant as inferRawDiscriminant); + const parsedDiscriminant = + typeof discriminant === "string" + ? discriminant + : (discriminant.parsedDiscriminant as inferParsedDiscriminant); + + const discriminantValueSchema = enum_(keys(union) as string[]); + + const baseSchema: BaseSchema, inferParsedUnion> = { + parse: (raw, opts) => { + return transformAndValidateUnion({ + value: raw, + discriminant: rawDiscriminant, + transformedDiscriminant: parsedDiscriminant, + transformDiscriminantValue: (discriminantValue) => + discriminantValueSchema.parse(discriminantValue, { + allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), rawDiscriminant], + }), + getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], + allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, + transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => + additionalPropertiesSchema.parse(additionalProperties, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + json: (parsed, opts) => { + return transformAndValidateUnion({ + value: parsed, + discriminant: parsedDiscriminant, + transformedDiscriminant: rawDiscriminant, + transformDiscriminantValue: (discriminantValue) => + discriminantValueSchema.json(discriminantValue, { + allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, + breadcrumbsPrefix: [...(opts?.breadcrumbsPrefix ?? []), parsedDiscriminant], + }), + getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], + allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, + transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => + additionalPropertiesSchema.json(additionalProperties, opts), + breadcrumbsPrefix: opts?.breadcrumbsPrefix, + }); + }, + getType: () => SchemaType.UNION, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + ...getObjectLikeUtils(baseSchema), + }; +} + +function transformAndValidateUnion< + TransformedDiscriminant extends string, + TransformedDiscriminantValue extends string, + TransformedAdditionalProperties +>({ + value, + discriminant, + transformedDiscriminant, + transformDiscriminantValue, + getAdditionalPropertiesSchema, + allowUnrecognizedUnionMembers = false, + transformAdditionalProperties, + breadcrumbsPrefix = [], +}: { + value: unknown; + discriminant: string; + transformedDiscriminant: TransformedDiscriminant; + transformDiscriminantValue: (discriminantValue: unknown) => MaybeValid; + getAdditionalPropertiesSchema: (discriminantValue: string) => ObjectSchema | undefined; + allowUnrecognizedUnionMembers: boolean | undefined; + transformAdditionalProperties: ( + additionalProperties: unknown, + additionalPropertiesSchema: ObjectSchema + ) => MaybeValid; + breadcrumbsPrefix: string[] | undefined; +}): MaybeValid & TransformedAdditionalProperties> { + if (!isPlainObject(value)) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: getErrorMessageForIncorrectType(value, "object"), + }, + ], + }; + } + + const { [discriminant]: discriminantValue, ...additionalProperties } = value; + + if (discriminantValue == null) { + return { + ok: false, + errors: [ + { + path: breadcrumbsPrefix, + message: `Missing discriminant ("${discriminant}")`, + }, + ], + }; + } + + const transformedDiscriminantValue = transformDiscriminantValue(discriminantValue); + if (!transformedDiscriminantValue.ok) { + return { + ok: false, + errors: transformedDiscriminantValue.errors, + }; + } + + const additionalPropertiesSchema = getAdditionalPropertiesSchema(transformedDiscriminantValue.value); + + if (additionalPropertiesSchema == null) { + if (allowUnrecognizedUnionMembers) { + return { + ok: true, + value: { + [transformedDiscriminant]: transformedDiscriminantValue.value, + ...additionalProperties, + } as Record & TransformedAdditionalProperties, + }; + } else { + return { + ok: false, + errors: [ + { + path: [...breadcrumbsPrefix, discriminant], + message: "Unexpected discriminant value", + }, + ], + }; + } + } + + const transformedAdditionalProperties = transformAdditionalProperties( + additionalProperties, + additionalPropertiesSchema + ); + if (!transformedAdditionalProperties.ok) { + return transformedAdditionalProperties; + } + + return { + ok: true, + value: { + [transformedDiscriminant]: discriminantValue, + ...transformedAdditionalProperties.value, + } as Record & TransformedAdditionalProperties, + }; +} diff --git a/src/core/schemas/index.ts b/src/core/schemas/index.ts new file mode 100644 index 0000000..5429d8b --- /dev/null +++ b/src/core/schemas/index.ts @@ -0,0 +1,2 @@ +export * from "./builders"; +export type { inferParsed, inferRaw, Schema, SchemaOptions } from "./Schema"; diff --git a/src/core/schemas/utils/MaybePromise.ts b/src/core/schemas/utils/MaybePromise.ts new file mode 100644 index 0000000..9cd354b --- /dev/null +++ b/src/core/schemas/utils/MaybePromise.ts @@ -0,0 +1 @@ +export type MaybePromise = T | Promise; diff --git a/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts b/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts new file mode 100644 index 0000000..4111d70 --- /dev/null +++ b/src/core/schemas/utils/addQuestionMarksToNullableProperties.ts @@ -0,0 +1,15 @@ +export type addQuestionMarksToNullableProperties = { + [K in OptionalKeys]?: T[K]; +} & Pick>; + +export type OptionalKeys = { + [K in keyof T]-?: undefined extends T[K] + ? K + : null extends T[K] + ? K + : 1 extends (any extends T[K] ? 0 : 1) + ? never + : K; +}[keyof T]; + +export type RequiredKeys = Exclude>; diff --git a/src/core/schemas/utils/createIdentitySchemaCreator.ts b/src/core/schemas/utils/createIdentitySchemaCreator.ts new file mode 100644 index 0000000..de107cf --- /dev/null +++ b/src/core/schemas/utils/createIdentitySchemaCreator.ts @@ -0,0 +1,21 @@ +import { getSchemaUtils } from "../builders/schema-utils"; +import { BaseSchema, MaybeValid, Schema, SchemaOptions, SchemaType } from "../Schema"; +import { maybeSkipValidation } from "./maybeSkipValidation"; + +export function createIdentitySchemaCreator( + schemaType: SchemaType, + validate: (value: unknown, opts?: SchemaOptions) => MaybeValid +): () => Schema { + return () => { + const baseSchema: BaseSchema = { + parse: validate, + json: validate, + getType: () => schemaType, + }; + + return { + ...maybeSkipValidation(baseSchema), + ...getSchemaUtils(baseSchema), + }; + }; +} diff --git a/src/core/schemas/utils/entries.ts b/src/core/schemas/utils/entries.ts new file mode 100644 index 0000000..e122952 --- /dev/null +++ b/src/core/schemas/utils/entries.ts @@ -0,0 +1,3 @@ +export function entries(object: T): [keyof T, T[keyof T]][] { + return Object.entries(object) as [keyof T, T[keyof T]][]; +} diff --git a/src/core/schemas/utils/filterObject.ts b/src/core/schemas/utils/filterObject.ts new file mode 100644 index 0000000..2c25a34 --- /dev/null +++ b/src/core/schemas/utils/filterObject.ts @@ -0,0 +1,10 @@ +export function filterObject(obj: T, keysToInclude: K[]): Pick { + const keysToIncludeSet = new Set(keysToInclude); + return Object.entries(obj).reduce((acc, [key, value]) => { + if (keysToIncludeSet.has(key as K)) { + acc[key as K] = value; + } + return acc; + // eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter + }, {} as Pick); +} diff --git a/src/core/schemas/utils/getErrorMessageForIncorrectType.ts b/src/core/schemas/utils/getErrorMessageForIncorrectType.ts new file mode 100644 index 0000000..1a5c310 --- /dev/null +++ b/src/core/schemas/utils/getErrorMessageForIncorrectType.ts @@ -0,0 +1,25 @@ +export function getErrorMessageForIncorrectType(value: unknown, expectedType: string): string { + return `Expected ${expectedType}. Received ${getTypeAsString(value)}.`; +} + +function getTypeAsString(value: unknown): string { + if (Array.isArray(value)) { + return "list"; + } + if (value === null) { + return "null"; + } + if (value instanceof BigInt) { + return "BigInt"; + } + switch (typeof value) { + case "string": + return `"${value}"`; + case "bigint": + case "number": + case "boolean": + case "undefined": + return `${value}`; + } + return typeof value; +} diff --git a/src/core/schemas/utils/isPlainObject.ts b/src/core/schemas/utils/isPlainObject.ts new file mode 100644 index 0000000..db82a72 --- /dev/null +++ b/src/core/schemas/utils/isPlainObject.ts @@ -0,0 +1,17 @@ +// borrowed from https://github.com/lodash/lodash/blob/master/isPlainObject.js +export function isPlainObject(value: unknown): value is Record { + if (typeof value !== "object" || value === null) { + return false; + } + + if (Object.getPrototypeOf(value) === null) { + return true; + } + + let proto = value; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + + return Object.getPrototypeOf(value) === proto; +} diff --git a/src/core/schemas/utils/keys.ts b/src/core/schemas/utils/keys.ts new file mode 100644 index 0000000..0186709 --- /dev/null +++ b/src/core/schemas/utils/keys.ts @@ -0,0 +1,3 @@ +export function keys(object: T): (keyof T)[] { + return Object.keys(object) as (keyof T)[]; +} diff --git a/src/core/schemas/utils/maybeSkipValidation.ts b/src/core/schemas/utils/maybeSkipValidation.ts new file mode 100644 index 0000000..86c07ab --- /dev/null +++ b/src/core/schemas/utils/maybeSkipValidation.ts @@ -0,0 +1,38 @@ +import { BaseSchema, MaybeValid, SchemaOptions } from "../Schema"; + +export function maybeSkipValidation, Raw, Parsed>(schema: S): S { + return { + ...schema, + json: transformAndMaybeSkipValidation(schema.json), + parse: transformAndMaybeSkipValidation(schema.parse), + }; +} + +function transformAndMaybeSkipValidation( + transform: (value: unknown, opts?: SchemaOptions) => MaybeValid +): (value: unknown, opts?: SchemaOptions) => MaybeValid { + return (value, opts): MaybeValid => { + const transformed = transform(value, opts); + const { skipValidation = false } = opts ?? {}; + if (!transformed.ok && skipValidation) { + // eslint-disable-next-line no-console + console.warn( + [ + "Failed to validate.", + ...transformed.errors.map( + (error) => + " - " + + (error.path.length > 0 ? `${error.path.join(".")}: ${error.message}` : error.message) + ), + ].join("\n") + ); + + return { + ok: true, + value: value as T, + }; + } else { + return transformed; + } + }; +} diff --git a/src/core/schemas/utils/partition.ts b/src/core/schemas/utils/partition.ts new file mode 100644 index 0000000..f58d6f3 --- /dev/null +++ b/src/core/schemas/utils/partition.ts @@ -0,0 +1,12 @@ +export function partition(items: readonly T[], predicate: (item: T) => boolean): [T[], T[]] { + const trueItems: T[] = [], + falseItems: T[] = []; + for (const item of items) { + if (predicate(item)) { + trueItems.push(item); + } else { + falseItems.push(item); + } + } + return [trueItems, falseItems]; +} diff --git a/src/core/websocket/events.ts b/src/core/websocket/events.ts new file mode 100644 index 0000000..87de627 --- /dev/null +++ b/src/core/websocket/events.ts @@ -0,0 +1,42 @@ +export class Event { + public target: any; + public type: string; + constructor(type: string, target: any) { + this.target = target; + this.type = type; + } +} + +export class ErrorEvent extends Event { + public message: string; + public error: Error; + constructor(error: Error, target: any) { + super("error", target); + this.message = error.message; + this.error = error; + } +} + +export class CloseEvent extends Event { + public code: number; + public reason: string; + public wasClean = true; + constructor(code = 1000, reason = "", target: any) { + super("close", target); + this.code = code; + this.reason = reason; + } +} +export interface WebSocketEventMap { + close: CloseEvent; + error: ErrorEvent; + message: MessageEvent; + open: Event; +} + +export interface WebSocketEventListenerMap { + close: (event: CloseEvent) => void | { handleEvent: (event: CloseEvent) => void }; + error: (event: ErrorEvent) => void | { handleEvent: (event: ErrorEvent) => void }; + message: (event: MessageEvent) => void | { handleEvent: (event: MessageEvent) => void }; + open: (event: Event) => void | { handleEvent: (event: Event) => void }; +} diff --git a/src/core/websocket/index.ts b/src/core/websocket/index.ts new file mode 100644 index 0000000..8f12dc7 --- /dev/null +++ b/src/core/websocket/index.ts @@ -0,0 +1 @@ +export * from "./ws"; diff --git a/src/core/websocket/ws.ts b/src/core/websocket/ws.ts new file mode 100644 index 0000000..4932b4a --- /dev/null +++ b/src/core/websocket/ws.ts @@ -0,0 +1,513 @@ +import { RUNTIME } from "../runtime"; +import * as Events from "./events"; +import { WebSocket as NodeWebSocket } from "ws"; + +const getGlobalWebSocket = (): WebSocket | undefined => { + if (typeof WebSocket !== "undefined") { + // @ts-ignore + return WebSocket; + } else if (RUNTIME.type === "node") { + return NodeWebSocket as unknown as WebSocket; + } + return undefined; +}; + +/** + * Returns true if given argument looks like a WebSocket class + */ +const isWebSocket = (w: any) => typeof w !== "undefined" && !!w && w.CLOSING === 2; + +export type Event = Events.Event; +export type ErrorEvent = Events.ErrorEvent; +export type CloseEvent = Events.CloseEvent; + +export type Options = { + WebSocket?: any; + maxReconnectionDelay?: number; + minReconnectionDelay?: number; + reconnectionDelayGrowFactor?: number; + minUptime?: number; + connectionTimeout?: number; + maxRetries?: number; + maxEnqueuedMessages?: number; + startClosed?: boolean; + debug?: boolean; +}; + +const DEFAULT = { + maxReconnectionDelay: 10000, + minReconnectionDelay: 1000 + Math.random() * 4000, + minUptime: 5000, + reconnectionDelayGrowFactor: 1.3, + connectionTimeout: 4000, + maxRetries: Infinity, + maxEnqueuedMessages: Infinity, + startClosed: false, + debug: false, +}; + +export type UrlProvider = string | (() => string) | (() => Promise); + +export type Message = string | ArrayBuffer | Blob | ArrayBufferView; + +export type ListenersMap = { + error: Array; + message: Array; + open: Array; + close: Array; +}; + +export class ReconnectingWebSocket { + private _ws?: WebSocket; + private _listeners: ListenersMap = { + error: [], + message: [], + open: [], + close: [], + }; + private _retryCount = -1; + private _uptimeTimeout: any; + private _connectTimeout: any; + private _shouldReconnect = true; + private _connectLock = false; + private _binaryType: BinaryType = "blob"; + private _closeCalled = false; + private _messageQueue: Message[] = []; + + private readonly _url: UrlProvider; + private readonly _protocols?: string | string[]; + private readonly _options: Options; + + constructor(url: UrlProvider, protocols?: string | string[], options: Options = {}) { + this._url = url; + this._protocols = protocols; + this._options = options; + if (this._options.startClosed) { + this._shouldReconnect = false; + } + this._connect(); + } + + static get CONNECTING() { + return 0; + } + static get OPEN() { + return 1; + } + static get CLOSING() { + return 2; + } + static get CLOSED() { + return 3; + } + + get CONNECTING() { + return ReconnectingWebSocket.CONNECTING; + } + get OPEN() { + return ReconnectingWebSocket.OPEN; + } + get CLOSING() { + return ReconnectingWebSocket.CLOSING; + } + get CLOSED() { + return ReconnectingWebSocket.CLOSED; + } + + get binaryType() { + return this._ws ? this._ws.binaryType : this._binaryType; + } + + set binaryType(value: BinaryType) { + this._binaryType = value; + if (this._ws) { + this._ws.binaryType = value; + } + } + + /** + * Returns the number or connection retries + */ + get retryCount(): number { + return Math.max(this._retryCount, 0); + } + + /** + * The number of bytes of data that have been queued using calls to send() but not yet + * transmitted to the network. This value resets to zero once all queued data has been sent. + * This value does not reset to zero when the connection is closed; if you keep calling send(), + * this will continue to climb. Read only + */ + get bufferedAmount(): number { + const bytes = this._messageQueue.reduce((acc, message) => { + if (typeof message === "string") { + acc += message.length; // not byte size + } else if (message instanceof Blob) { + acc += message.size; + } else { + acc += message.byteLength; + } + return acc; + }, 0); + return bytes + (this._ws ? this._ws.bufferedAmount : 0); + } + + /** + * The extensions selected by the server. This is currently only the empty string or a list of + * extensions as negotiated by the connection + */ + get extensions(): string { + return this._ws ? this._ws.extensions : ""; + } + + /** + * A string indicating the name of the sub-protocol the server selected; + * this will be one of the strings specified in the protocols parameter when creating the + * WebSocket object + */ + get protocol(): string { + return this._ws ? this._ws.protocol : ""; + } + + /** + * The current state of the connection; this is one of the Ready state constants + */ + get readyState(): number { + if (this._ws) { + return this._ws.readyState; + } + return this._options.startClosed ? ReconnectingWebSocket.CLOSED : ReconnectingWebSocket.CONNECTING; + } + + /** + * The URL as resolved by the constructor + */ + get url(): string { + return this._ws ? this._ws.url : ""; + } + + /** + * An event listener to be called when the WebSocket connection's readyState changes to CLOSED + */ + public onclose: ((event: Events.CloseEvent) => void) | null = null; + + /** + * An event listener to be called when an error occurs + */ + public onerror: ((event: Events.ErrorEvent) => void) | null = null; + + /** + * An event listener to be called when a message is received from the server + */ + public onmessage: ((event: MessageEvent) => void) | null = null; + + /** + * An event listener to be called when the WebSocket connection's readyState changes to OPEN; + * this indicates that the connection is ready to send and receive data + */ + public onopen: ((event: Event) => void) | null = null; + + /** + * Closes the WebSocket connection or connection attempt, if any. If the connection is already + * CLOSED, this method does nothing + */ + public close(code = 1000, reason?: string) { + this._closeCalled = true; + this._shouldReconnect = false; + this._clearTimeouts(); + if (!this._ws) { + this._debug("close enqueued: no ws instance"); + return; + } + if (this._ws.readyState === this.CLOSED) { + this._debug("close: already closed"); + return; + } + this._ws.close(code, reason); + } + + /** + * Closes the WebSocket connection or connection attempt and connects again. + * Resets retry counter; + */ + public reconnect(code?: number, reason?: string) { + this._shouldReconnect = true; + this._closeCalled = false; + this._retryCount = -1; + if (!this._ws || this._ws.readyState === this.CLOSED) { + this._connect(); + } else { + this._disconnect(code, reason); + this._connect(); + } + } + + /** + * Enqueue specified data to be transmitted to the server over the WebSocket connection + */ + public send(data: Message) { + if (this._ws && this._ws.readyState === this.OPEN) { + this._debug("send", data); + this._ws.send(data); + } else { + const { maxEnqueuedMessages = DEFAULT.maxEnqueuedMessages } = this._options; + if (this._messageQueue.length < maxEnqueuedMessages) { + this._debug("enqueue", data); + this._messageQueue.push(data); + } + } + } + + /** + * Register an event handler of a specific event type + */ + public addEventListener( + type: T, + listener: Events.WebSocketEventListenerMap[T] + ): void { + if (this._listeners[type]) { + // @ts-ignore + this._listeners[type].push(listener); + } + } + + public dispatchEvent(event: Event) { + const listeners = this._listeners[event.type as keyof Events.WebSocketEventListenerMap]; + if (listeners) { + for (const listener of listeners) { + this._callEventListener(event, listener); + } + } + return true; + } + + /** + * Removes an event listener + */ + public removeEventListener( + type: T, + listener: Events.WebSocketEventListenerMap[T] + ): void { + if (this._listeners[type]) { + // @ts-ignore + this._listeners[type] = this._listeners[type].filter( + // @ts-ignore + (l) => l !== listener + ); + } + } + + private _debug(...args: any[]) { + if (this._options.debug) { + // not using spread because compiled version uses Symbols + // tslint:disable-next-line + console.log.apply(console, ["RWS>", ...args]); + } + } + + private _getNextDelay() { + const { + reconnectionDelayGrowFactor = DEFAULT.reconnectionDelayGrowFactor, + minReconnectionDelay = DEFAULT.minReconnectionDelay, + maxReconnectionDelay = DEFAULT.maxReconnectionDelay, + } = this._options; + let delay = 0; + if (this._retryCount > 0) { + delay = minReconnectionDelay * Math.pow(reconnectionDelayGrowFactor, this._retryCount - 1); + if (delay > maxReconnectionDelay) { + delay = maxReconnectionDelay; + } + } + this._debug("next delay", delay); + return delay; + } + + private _wait(): Promise { + return new Promise((resolve) => { + setTimeout(resolve, this._getNextDelay()); + }); + } + + private _getNextUrl(urlProvider: UrlProvider): Promise { + if (typeof urlProvider === "string") { + return Promise.resolve(urlProvider); + } + if (typeof urlProvider === "function") { + const url = urlProvider(); + if (typeof url === "string") { + return Promise.resolve(url); + } + // @ts-ignore redundant check + if (url.then) { + return url; + } + } + throw Error("Invalid URL"); + } + + private _connect() { + if (this._connectLock || !this._shouldReconnect) { + return; + } + this._connectLock = true; + + const { + maxRetries = DEFAULT.maxRetries, + connectionTimeout = DEFAULT.connectionTimeout, + WebSocket = getGlobalWebSocket(), + } = this._options; + + if (this._retryCount >= maxRetries) { + this._debug("max retries reached", this._retryCount, ">=", maxRetries); + return; + } + + this._retryCount++; + + this._debug("connect", this._retryCount); + this._removeListeners(); + if (!isWebSocket(WebSocket)) { + throw Error("No valid WebSocket class provided"); + } + this._wait() + .then(() => this._getNextUrl(this._url)) + .then((url) => { + // close could be called before creating the ws + if (this._closeCalled) { + return; + } + this._debug("connect", { url, protocols: this._protocols }); + this._ws = this._protocols ? new WebSocket(url, this._protocols) : new WebSocket(url); + this._ws!.binaryType = this._binaryType; + this._connectLock = false; + this._addListeners(); + + this._connectTimeout = setTimeout(() => this._handleTimeout(), connectionTimeout); + }); + } + + private _handleTimeout() { + this._debug("timeout event"); + this._handleError(new Events.ErrorEvent(Error("TIMEOUT"), this)); + } + + private _disconnect(code = 1000, reason?: string) { + this._clearTimeouts(); + if (!this._ws) { + return; + } + this._removeListeners(); + try { + this._ws.close(code, reason); + this._handleClose(new Events.CloseEvent(code, reason, this)); + } catch (error) { + // ignore + } + } + + private _acceptOpen() { + this._debug("accept open"); + this._retryCount = 0; + } + + private _callEventListener( + event: Events.WebSocketEventMap[T], + listener: Events.WebSocketEventListenerMap[T] + ) { + if ("handleEvent" in listener) { + // @ts-ignore + listener.handleEvent(event); + } else { + // @ts-ignore + listener(event); + } + } + + private _handleOpen = (event: Event) => { + this._debug("open event"); + const { minUptime = DEFAULT.minUptime } = this._options; + + clearTimeout(this._connectTimeout); + this._uptimeTimeout = setTimeout(() => this._acceptOpen(), minUptime); + + this._ws!.binaryType = this._binaryType; + + // send enqueued messages (messages sent before websocket open event) + this._messageQueue.forEach((message) => this._ws?.send(message)); + this._messageQueue = []; + + if (this.onopen) { + this.onopen(event); + } + this._listeners.open.forEach((listener) => this._callEventListener(event, listener)); + }; + + private _handleMessage = (event: MessageEvent) => { + this._debug("message event"); + + if (this.onmessage) { + this.onmessage(event); + } + this._listeners.message.forEach((listener) => this._callEventListener(event, listener)); + }; + + private _handleError = (event: Events.ErrorEvent) => { + this._debug("error event", event.message); + this._disconnect(undefined, event.message === "TIMEOUT" ? "timeout" : undefined); + + if (this.onerror) { + this.onerror(event); + } + this._debug("exec error listeners"); + this._listeners.error.forEach((listener) => this._callEventListener(event, listener)); + + this._connect(); + }; + + private _handleClose = (event: Events.CloseEvent) => { + this._debug("close event"); + this._clearTimeouts(); + + if (event.code === 1000) { + this._shouldReconnect = false; + } + + if (this._shouldReconnect) { + this._connect(); + } + + if (this.onclose) { + this.onclose(event); + } + this._listeners.close.forEach((listener) => this._callEventListener(event, listener)); + }; + + private _removeListeners() { + if (!this._ws) { + return; + } + this._debug("removeListeners"); + this._ws.removeEventListener("open", this._handleOpen); + this._ws.removeEventListener("close", this._handleClose); + this._ws.removeEventListener("message", this._handleMessage); + // @ts-ignore + this._ws.removeEventListener("error", this._handleError); + } + + private _addListeners() { + if (!this._ws) { + return; + } + this._debug("addListeners"); + this._ws.addEventListener("open", this._handleOpen); + this._ws.addEventListener("close", this._handleClose); + this._ws.addEventListener("message", this._handleMessage); + // @ts-ignore + this._ws.addEventListener("error", this._handleError); + } + + private _clearTimeouts() { + clearTimeout(this._connectTimeout); + clearTimeout(this._uptimeTimeout); + } +} diff --git a/src/index.ts b/src/index.ts index 7022611..db04fb4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ export * as Cartesia from "./api"; -export { CartesiaClient } from "./Client"; +export { CartesiaClient } from "./wrapper/Client"; export { CartesiaEnvironment } from "./environments"; export { CartesiaError, CartesiaTimeoutError } from "./errors"; diff --git a/src/serialization/index.ts b/src/serialization/index.ts new file mode 100644 index 0000000..3e5335f --- /dev/null +++ b/src/serialization/index.ts @@ -0,0 +1 @@ +export * from "./resources"; diff --git a/src/serialization/resources/apiStatus/index.ts b/src/serialization/resources/apiStatus/index.ts new file mode 100644 index 0000000..eea524d --- /dev/null +++ b/src/serialization/resources/apiStatus/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/src/serialization/resources/apiStatus/types/ApiInfo.ts b/src/serialization/resources/apiStatus/types/ApiInfo.ts new file mode 100644 index 0000000..18dd51e --- /dev/null +++ b/src/serialization/resources/apiStatus/types/ApiInfo.ts @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const ApiInfo: core.serialization.ObjectSchema = + core.serialization.object({ + ok: core.serialization.boolean(), + version: core.serialization.string(), + }); + +export declare namespace ApiInfo { + interface Raw { + ok: boolean; + version: string; + } +} diff --git a/src/serialization/resources/apiStatus/types/index.ts b/src/serialization/resources/apiStatus/types/index.ts new file mode 100644 index 0000000..1ce4f3e --- /dev/null +++ b/src/serialization/resources/apiStatus/types/index.ts @@ -0,0 +1 @@ +export * from "./ApiInfo"; diff --git a/src/serialization/resources/embedding/index.ts b/src/serialization/resources/embedding/index.ts new file mode 100644 index 0000000..eea524d --- /dev/null +++ b/src/serialization/resources/embedding/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/src/serialization/resources/embedding/types/Embedding.ts b/src/serialization/resources/embedding/types/Embedding.ts new file mode 100644 index 0000000..8bc8e01 --- /dev/null +++ b/src/serialization/resources/embedding/types/Embedding.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const Embedding: core.serialization.Schema = + core.serialization.list(core.serialization.number()); + +export declare namespace Embedding { + type Raw = number[]; +} diff --git a/src/serialization/resources/embedding/types/index.ts b/src/serialization/resources/embedding/types/index.ts new file mode 100644 index 0000000..cd604e1 --- /dev/null +++ b/src/serialization/resources/embedding/types/index.ts @@ -0,0 +1 @@ +export * from "./Embedding"; diff --git a/src/serialization/resources/index.ts b/src/serialization/resources/index.ts new file mode 100644 index 0000000..61ee374 --- /dev/null +++ b/src/serialization/resources/index.ts @@ -0,0 +1,10 @@ +export * as apiStatus from "./apiStatus"; +export * from "./apiStatus/types"; +export * as embedding from "./embedding"; +export * from "./embedding/types"; +export * as tts from "./tts"; +export * from "./tts/types"; +export * as voiceChanger from "./voiceChanger"; +export * from "./voiceChanger/types"; +export * as voices from "./voices"; +export * from "./voices/types"; diff --git a/src/serialization/resources/tts/index.ts b/src/serialization/resources/tts/index.ts new file mode 100644 index 0000000..eea524d --- /dev/null +++ b/src/serialization/resources/tts/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/src/serialization/resources/tts/types/CancelContextRequest.ts b/src/serialization/resources/tts/types/CancelContextRequest.ts new file mode 100644 index 0000000..47e2abc --- /dev/null +++ b/src/serialization/resources/tts/types/CancelContextRequest.ts @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { ContextId } from "./ContextId"; + +export const CancelContextRequest: core.serialization.ObjectSchema< + serializers.CancelContextRequest.Raw, + Cartesia.CancelContextRequest +> = core.serialization.object({ + contextId: core.serialization.property("context_id", ContextId), + cancel: core.serialization.booleanLiteral(true), +}); + +export declare namespace CancelContextRequest { + interface Raw { + context_id: ContextId.Raw; + cancel: true; + } +} diff --git a/src/serialization/resources/tts/types/ContextId.ts b/src/serialization/resources/tts/types/ContextId.ts new file mode 100644 index 0000000..86d5619 --- /dev/null +++ b/src/serialization/resources/tts/types/ContextId.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const ContextId: core.serialization.Schema = + core.serialization.string(); + +export declare namespace ContextId { + type Raw = string; +} diff --git a/src/serialization/resources/tts/types/Controls.ts b/src/serialization/resources/tts/types/Controls.ts new file mode 100644 index 0000000..b89bb6d --- /dev/null +++ b/src/serialization/resources/tts/types/Controls.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { Speed } from "./Speed"; +import { Emotion } from "./Emotion"; + +export const Controls: core.serialization.ObjectSchema = + core.serialization.object({ + speed: Speed, + emotion: Emotion, + }); + +export declare namespace Controls { + interface Raw { + speed: Speed.Raw; + emotion: Emotion.Raw; + } +} diff --git a/src/serialization/resources/tts/types/Emotion.ts b/src/serialization/resources/tts/types/Emotion.ts new file mode 100644 index 0000000..910109d --- /dev/null +++ b/src/serialization/resources/tts/types/Emotion.ts @@ -0,0 +1,54 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const Emotion: core.serialization.Schema = core.serialization.enum_([ + "anger:lowest", + "anger:low", + "anger", + "anger:high", + "anger:highest", + "positivity:lowest", + "positivity:low", + "positivity", + "positivity:high", + "positivity:highest", + "surprise:lowest", + "surprise:high", + "surprise:highest", + "sadness:lowest", + "sadness:low", + "sadness", + "curiosity:low", + "curiosity", + "curiosity:high", + "curiosity:highest", +]); + +export declare namespace Emotion { + type Raw = + | "anger:lowest" + | "anger:low" + | "anger" + | "anger:high" + | "anger:highest" + | "positivity:lowest" + | "positivity:low" + | "positivity" + | "positivity:high" + | "positivity:highest" + | "surprise:lowest" + | "surprise:high" + | "surprise:highest" + | "sadness:lowest" + | "sadness:low" + | "sadness" + | "curiosity:low" + | "curiosity" + | "curiosity:high" + | "curiosity:highest"; +} diff --git a/src/serialization/resources/tts/types/GenerationRequest.ts b/src/serialization/resources/tts/types/GenerationRequest.ts new file mode 100644 index 0000000..d985791 --- /dev/null +++ b/src/serialization/resources/tts/types/GenerationRequest.ts @@ -0,0 +1,40 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { TtsRequestVoiceSpecifier } from "./TtsRequestVoiceSpecifier"; +import { SupportedLanguage } from "./SupportedLanguage"; +import { WebSocketRawOutputFormat } from "./WebSocketRawOutputFormat"; +import { ContextId } from "./ContextId"; + +export const GenerationRequest: core.serialization.ObjectSchema< + serializers.GenerationRequest.Raw, + Cartesia.GenerationRequest +> = core.serialization.object({ + modelId: core.serialization.property("model_id", core.serialization.string()), + transcript: core.serialization.string(), + voice: TtsRequestVoiceSpecifier, + language: SupportedLanguage.optional(), + outputFormat: core.serialization.property("output_format", WebSocketRawOutputFormat), + duration: core.serialization.number().optional(), + contextId: core.serialization.property("context_id", ContextId), + continue: core.serialization.boolean().optional(), + addTimestamps: core.serialization.property("add_timestamps", core.serialization.boolean().optional()), +}); + +export declare namespace GenerationRequest { + interface Raw { + model_id: string; + transcript: string; + voice: TtsRequestVoiceSpecifier.Raw; + language?: SupportedLanguage.Raw | null; + output_format: WebSocketRawOutputFormat.Raw; + duration?: number | null; + context_id: ContextId.Raw; + continue?: boolean | null; + add_timestamps?: boolean | null; + } +} diff --git a/src/serialization/resources/tts/types/Mp3OutputFormat.ts b/src/serialization/resources/tts/types/Mp3OutputFormat.ts new file mode 100644 index 0000000..d099ddc --- /dev/null +++ b/src/serialization/resources/tts/types/Mp3OutputFormat.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const Mp3OutputFormat: core.serialization.ObjectSchema< + serializers.Mp3OutputFormat.Raw, + Cartesia.Mp3OutputFormat +> = core.serialization.object({ + sampleRate: core.serialization.property("sample_rate", core.serialization.number()), + bitRate: core.serialization.property("bit_rate", core.serialization.number()), +}); + +export declare namespace Mp3OutputFormat { + interface Raw { + sample_rate: number; + bit_rate: number; + } +} diff --git a/src/serialization/resources/tts/types/NaturalSpecifier.ts b/src/serialization/resources/tts/types/NaturalSpecifier.ts new file mode 100644 index 0000000..865f64a --- /dev/null +++ b/src/serialization/resources/tts/types/NaturalSpecifier.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const NaturalSpecifier: core.serialization.Schema = + core.serialization.enum_(["slowest", "slow", "normal", "fast", "fastest"]); + +export declare namespace NaturalSpecifier { + type Raw = "slowest" | "slow" | "normal" | "fast" | "fastest"; +} diff --git a/src/serialization/resources/tts/types/NumericalSpecifier.ts b/src/serialization/resources/tts/types/NumericalSpecifier.ts new file mode 100644 index 0000000..02a7731 --- /dev/null +++ b/src/serialization/resources/tts/types/NumericalSpecifier.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const NumericalSpecifier: core.serialization.Schema< + serializers.NumericalSpecifier.Raw, + Cartesia.NumericalSpecifier +> = core.serialization.number(); + +export declare namespace NumericalSpecifier { + type Raw = number; +} diff --git a/src/serialization/resources/tts/types/OutputFormat.ts b/src/serialization/resources/tts/types/OutputFormat.ts new file mode 100644 index 0000000..fff85d0 --- /dev/null +++ b/src/serialization/resources/tts/types/OutputFormat.ts @@ -0,0 +1,38 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { RawOutputFormat } from "./RawOutputFormat"; +import { WavOutputFormat } from "./WavOutputFormat"; +import { Mp3OutputFormat } from "./Mp3OutputFormat"; + +export const OutputFormat: core.serialization.Schema = + core.serialization + .union("container", { + raw: RawOutputFormat, + wav: WavOutputFormat, + mp3: Mp3OutputFormat, + }) + .transform({ + transform: (value) => value, + untransform: (value) => value, + }); + +export declare namespace OutputFormat { + type Raw = OutputFormat._Raw | OutputFormat.Wav | OutputFormat.Mp3; + + interface _Raw extends RawOutputFormat.Raw { + container: "raw"; + } + + interface Wav extends WavOutputFormat.Raw { + container: "wav"; + } + + interface Mp3 extends Mp3OutputFormat.Raw { + container: "mp3"; + } +} diff --git a/src/serialization/resources/tts/types/RawEncoding.ts b/src/serialization/resources/tts/types/RawEncoding.ts new file mode 100644 index 0000000..508fdce --- /dev/null +++ b/src/serialization/resources/tts/types/RawEncoding.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const RawEncoding: core.serialization.Schema = + core.serialization.enum_(["pcm_f32le", "pcm_s16le", "pcm_mulaw", "pcm_alaw"]); + +export declare namespace RawEncoding { + type Raw = "pcm_f32le" | "pcm_s16le" | "pcm_mulaw" | "pcm_alaw"; +} diff --git a/src/serialization/resources/tts/types/RawOutputFormat.ts b/src/serialization/resources/tts/types/RawOutputFormat.ts new file mode 100644 index 0000000..62122f7 --- /dev/null +++ b/src/serialization/resources/tts/types/RawOutputFormat.ts @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { RawEncoding } from "./RawEncoding"; + +export const RawOutputFormat: core.serialization.ObjectSchema< + serializers.RawOutputFormat.Raw, + Cartesia.RawOutputFormat +> = core.serialization.object({ + encoding: RawEncoding, + sampleRate: core.serialization.property("sample_rate", core.serialization.number()), +}); + +export declare namespace RawOutputFormat { + interface Raw { + encoding: RawEncoding.Raw; + sample_rate: number; + } +} diff --git a/src/serialization/resources/tts/types/Speed.ts b/src/serialization/resources/tts/types/Speed.ts new file mode 100644 index 0000000..20343cf --- /dev/null +++ b/src/serialization/resources/tts/types/Speed.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { NumericalSpecifier } from "./NumericalSpecifier"; +import { NaturalSpecifier } from "./NaturalSpecifier"; + +export const Speed: core.serialization.Schema = + core.serialization.undiscriminatedUnion([NumericalSpecifier, NaturalSpecifier]); + +export declare namespace Speed { + type Raw = NumericalSpecifier.Raw | NaturalSpecifier.Raw; +} diff --git a/src/serialization/resources/tts/types/SupportedLanguage.ts b/src/serialization/resources/tts/types/SupportedLanguage.ts new file mode 100644 index 0000000..2065aa7 --- /dev/null +++ b/src/serialization/resources/tts/types/SupportedLanguage.ts @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const SupportedLanguage: core.serialization.Schema< + serializers.SupportedLanguage.Raw, + Cartesia.SupportedLanguage +> = core.serialization.enum_([ + "en", + "fr", + "de", + "es", + "pt", + "zh", + "ja", + "hi", + "it", + "ko", + "nl", + "pl", + "ru", + "sv", + "tr", +]); + +export declare namespace SupportedLanguage { + type Raw = "en" | "fr" | "de" | "es" | "pt" | "zh" | "ja" | "hi" | "it" | "ko" | "nl" | "pl" | "ru" | "sv" | "tr"; +} diff --git a/src/serialization/resources/tts/types/TtsRequest.ts b/src/serialization/resources/tts/types/TtsRequest.ts new file mode 100644 index 0000000..685a0b5 --- /dev/null +++ b/src/serialization/resources/tts/types/TtsRequest.ts @@ -0,0 +1,31 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { TtsRequestVoiceSpecifier } from "./TtsRequestVoiceSpecifier"; +import { SupportedLanguage } from "./SupportedLanguage"; +import { OutputFormat } from "./OutputFormat"; + +export const TtsRequest: core.serialization.ObjectSchema = + core.serialization.object({ + modelId: core.serialization.property("model_id", core.serialization.string()), + transcript: core.serialization.string(), + voice: TtsRequestVoiceSpecifier, + language: SupportedLanguage.optional(), + outputFormat: core.serialization.property("output_format", OutputFormat), + duration: core.serialization.number().optional(), + }); + +export declare namespace TtsRequest { + interface Raw { + model_id: string; + transcript: string; + voice: TtsRequestVoiceSpecifier.Raw; + language?: SupportedLanguage.Raw | null; + output_format: OutputFormat.Raw; + duration?: number | null; + } +} diff --git a/src/serialization/resources/tts/types/TtsRequestEmbeddingSpecifier.ts b/src/serialization/resources/tts/types/TtsRequestEmbeddingSpecifier.ts new file mode 100644 index 0000000..83a0a6c --- /dev/null +++ b/src/serialization/resources/tts/types/TtsRequestEmbeddingSpecifier.ts @@ -0,0 +1,26 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { Embedding } from "../../embedding/types/Embedding"; +import { Controls } from "./Controls"; + +export const TtsRequestEmbeddingSpecifier: core.serialization.ObjectSchema< + serializers.TtsRequestEmbeddingSpecifier.Raw, + Cartesia.TtsRequestEmbeddingSpecifier +> = core.serialization.object({ + mode: core.serialization.stringLiteral("embedding"), + embedding: Embedding, + experimentalControls: core.serialization.property("__experimental_controls", Controls.optional()), +}); + +export declare namespace TtsRequestEmbeddingSpecifier { + interface Raw { + mode: "embedding"; + embedding: Embedding.Raw; + __experimental_controls?: Controls.Raw | null; + } +} diff --git a/src/serialization/resources/tts/types/TtsRequestIdSpecifier.ts b/src/serialization/resources/tts/types/TtsRequestIdSpecifier.ts new file mode 100644 index 0000000..26ac33c --- /dev/null +++ b/src/serialization/resources/tts/types/TtsRequestIdSpecifier.ts @@ -0,0 +1,26 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { VoiceId } from "../../voices/types/VoiceId"; +import { Controls } from "./Controls"; + +export const TtsRequestIdSpecifier: core.serialization.ObjectSchema< + serializers.TtsRequestIdSpecifier.Raw, + Cartesia.TtsRequestIdSpecifier +> = core.serialization.object({ + mode: core.serialization.stringLiteral("id"), + id: VoiceId, + experimentalControls: core.serialization.property("__experimental_controls", Controls.optional()), +}); + +export declare namespace TtsRequestIdSpecifier { + interface Raw { + mode: "id"; + id: VoiceId.Raw; + __experimental_controls?: Controls.Raw | null; + } +} diff --git a/src/serialization/resources/tts/types/TtsRequestVoiceSpecifier.ts b/src/serialization/resources/tts/types/TtsRequestVoiceSpecifier.ts new file mode 100644 index 0000000..c2a69c0 --- /dev/null +++ b/src/serialization/resources/tts/types/TtsRequestVoiceSpecifier.ts @@ -0,0 +1,18 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { TtsRequestIdSpecifier } from "./TtsRequestIdSpecifier"; +import { TtsRequestEmbeddingSpecifier } from "./TtsRequestEmbeddingSpecifier"; + +export const TtsRequestVoiceSpecifier: core.serialization.Schema< + serializers.TtsRequestVoiceSpecifier.Raw, + Cartesia.TtsRequestVoiceSpecifier +> = core.serialization.undiscriminatedUnion([TtsRequestIdSpecifier, TtsRequestEmbeddingSpecifier]); + +export declare namespace TtsRequestVoiceSpecifier { + type Raw = TtsRequestIdSpecifier.Raw | TtsRequestEmbeddingSpecifier.Raw; +} diff --git a/src/serialization/resources/tts/types/WavOutputFormat.ts b/src/serialization/resources/tts/types/WavOutputFormat.ts new file mode 100644 index 0000000..2ddd0ad --- /dev/null +++ b/src/serialization/resources/tts/types/WavOutputFormat.ts @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { RawOutputFormat } from "./RawOutputFormat"; + +export const WavOutputFormat: core.serialization.ObjectSchema< + serializers.WavOutputFormat.Raw, + Cartesia.WavOutputFormat +> = core.serialization.object({}).extend(RawOutputFormat); + +export declare namespace WavOutputFormat { + interface Raw extends RawOutputFormat.Raw {} +} diff --git a/src/serialization/resources/tts/types/WebSocketBaseResponse.ts b/src/serialization/resources/tts/types/WebSocketBaseResponse.ts new file mode 100644 index 0000000..2bdb226 --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketBaseResponse.ts @@ -0,0 +1,25 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { ContextId } from "./ContextId"; + +export const WebSocketBaseResponse: core.serialization.ObjectSchema< + serializers.WebSocketBaseResponse.Raw, + Cartesia.WebSocketBaseResponse +> = core.serialization.object({ + contextId: core.serialization.property("context_id", ContextId.optional()), + statusCode: core.serialization.property("status_code", core.serialization.number()), + done: core.serialization.boolean(), +}); + +export declare namespace WebSocketBaseResponse { + interface Raw { + context_id?: ContextId.Raw | null; + status_code: number; + done: boolean; + } +} diff --git a/src/serialization/resources/tts/types/WebSocketChunkResponse.ts b/src/serialization/resources/tts/types/WebSocketChunkResponse.ts new file mode 100644 index 0000000..47b4a17 --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketChunkResponse.ts @@ -0,0 +1,25 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { WebSocketBaseResponse } from "./WebSocketBaseResponse"; + +export const WebSocketChunkResponse: core.serialization.ObjectSchema< + serializers.WebSocketChunkResponse.Raw, + Cartesia.WebSocketChunkResponse +> = core.serialization + .object({ + data: core.serialization.string(), + stepTime: core.serialization.property("step_time", core.serialization.number()), + }) + .extend(WebSocketBaseResponse); + +export declare namespace WebSocketChunkResponse { + interface Raw extends WebSocketBaseResponse.Raw { + data: string; + step_time: number; + } +} diff --git a/src/serialization/resources/tts/types/WebSocketDoneResponse.ts b/src/serialization/resources/tts/types/WebSocketDoneResponse.ts new file mode 100644 index 0000000..664b446 --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketDoneResponse.ts @@ -0,0 +1,17 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { WebSocketBaseResponse } from "./WebSocketBaseResponse"; + +export const WebSocketDoneResponse: core.serialization.ObjectSchema< + serializers.WebSocketDoneResponse.Raw, + Cartesia.WebSocketDoneResponse +> = core.serialization.object({}).extend(WebSocketBaseResponse); + +export declare namespace WebSocketDoneResponse { + interface Raw extends WebSocketBaseResponse.Raw {} +} diff --git a/src/serialization/resources/tts/types/WebSocketErrorResponse.ts b/src/serialization/resources/tts/types/WebSocketErrorResponse.ts new file mode 100644 index 0000000..9db2694 --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketErrorResponse.ts @@ -0,0 +1,23 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { WebSocketBaseResponse } from "./WebSocketBaseResponse"; + +export const WebSocketErrorResponse: core.serialization.ObjectSchema< + serializers.WebSocketErrorResponse.Raw, + Cartesia.WebSocketErrorResponse +> = core.serialization + .object({ + error: core.serialization.string(), + }) + .extend(WebSocketBaseResponse); + +export declare namespace WebSocketErrorResponse { + interface Raw extends WebSocketBaseResponse.Raw { + error: string; + } +} diff --git a/src/serialization/resources/tts/types/WebSocketRawOutputFormat.ts b/src/serialization/resources/tts/types/WebSocketRawOutputFormat.ts new file mode 100644 index 0000000..44639aa --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketRawOutputFormat.ts @@ -0,0 +1,25 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { RawEncoding } from "./RawEncoding"; + +export const WebSocketRawOutputFormat: core.serialization.ObjectSchema< + serializers.WebSocketRawOutputFormat.Raw, + Cartesia.WebSocketRawOutputFormat +> = core.serialization.object({ + container: core.serialization.stringLiteral("raw"), + encoding: RawEncoding, + sampleRate: core.serialization.property("sample_rate", core.serialization.number()), +}); + +export declare namespace WebSocketRawOutputFormat { + interface Raw { + container: "raw"; + encoding: RawEncoding.Raw; + sample_rate: number; + } +} diff --git a/src/serialization/resources/tts/types/WebSocketRequest.ts b/src/serialization/resources/tts/types/WebSocketRequest.ts new file mode 100644 index 0000000..f55376c --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketRequest.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { GenerationRequest } from "./GenerationRequest"; +import { CancelContextRequest } from "./CancelContextRequest"; + +export const WebSocketRequest: core.serialization.Schema = + core.serialization.undiscriminatedUnion([GenerationRequest, CancelContextRequest]); + +export declare namespace WebSocketRequest { + type Raw = GenerationRequest.Raw | CancelContextRequest.Raw; +} diff --git a/src/serialization/resources/tts/types/WebSocketResponse.ts b/src/serialization/resources/tts/types/WebSocketResponse.ts new file mode 100644 index 0000000..315992c --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketResponse.ts @@ -0,0 +1,50 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { WebSocketChunkResponse } from "./WebSocketChunkResponse"; +import { WebSocketDoneResponse } from "./WebSocketDoneResponse"; +import { WebSocketTimestampsResponse } from "./WebSocketTimestampsResponse"; +import { WebSocketErrorResponse } from "./WebSocketErrorResponse"; + +export const WebSocketResponse: core.serialization.Schema< + serializers.WebSocketResponse.Raw, + Cartesia.WebSocketResponse +> = core.serialization + .union("type", { + chunk: WebSocketChunkResponse, + done: WebSocketDoneResponse, + timestamps: WebSocketTimestampsResponse, + error: WebSocketErrorResponse, + }) + .transform({ + transform: (value) => value, + untransform: (value) => value, + }); + +export declare namespace WebSocketResponse { + type Raw = + | WebSocketResponse.Chunk + | WebSocketResponse.Done + | WebSocketResponse.Timestamps + | WebSocketResponse.Error; + + interface Chunk extends WebSocketChunkResponse.Raw { + type: "chunk"; + } + + interface Done extends WebSocketDoneResponse.Raw { + type: "done"; + } + + interface Timestamps extends WebSocketTimestampsResponse.Raw { + type: "timestamps"; + } + + interface Error extends WebSocketErrorResponse.Raw { + type: "error"; + } +} diff --git a/src/serialization/resources/tts/types/WebSocketStreamOptions.ts b/src/serialization/resources/tts/types/WebSocketStreamOptions.ts new file mode 100644 index 0000000..9a1566c --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketStreamOptions.ts @@ -0,0 +1,20 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const WebSocketStreamOptions: core.serialization.ObjectSchema< + serializers.WebSocketStreamOptions.Raw, + Cartesia.WebSocketStreamOptions +> = core.serialization.object({ + timeout: core.serialization.number().optional(), +}); + +export declare namespace WebSocketStreamOptions { + interface Raw { + timeout?: number | null; + } +} diff --git a/src/serialization/resources/tts/types/WebSocketTimestampsResponse.ts b/src/serialization/resources/tts/types/WebSocketTimestampsResponse.ts new file mode 100644 index 0000000..12408b0 --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketTimestampsResponse.ts @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { WordTimestamps } from "./WordTimestamps"; +import { WebSocketBaseResponse } from "./WebSocketBaseResponse"; + +export const WebSocketTimestampsResponse: core.serialization.ObjectSchema< + serializers.WebSocketTimestampsResponse.Raw, + Cartesia.WebSocketTimestampsResponse +> = core.serialization + .object({ + wordTimestamps: core.serialization.property("word_timestamps", WordTimestamps.optional()), + }) + .extend(WebSocketBaseResponse); + +export declare namespace WebSocketTimestampsResponse { + interface Raw extends WebSocketBaseResponse.Raw { + word_timestamps?: WordTimestamps.Raw | null; + } +} diff --git a/src/serialization/resources/tts/types/WebSocketTtsOutput.ts b/src/serialization/resources/tts/types/WebSocketTtsOutput.ts new file mode 100644 index 0000000..c99916f --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketTtsOutput.ts @@ -0,0 +1,26 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { WordTimestamps } from "./WordTimestamps"; +import { ContextId } from "./ContextId"; + +export const WebSocketTtsOutput: core.serialization.ObjectSchema< + serializers.WebSocketTtsOutput.Raw, + Cartesia.WebSocketTtsOutput +> = core.serialization.object({ + wordTimestamps: core.serialization.property("word_timestamps", WordTimestamps.optional()), + audio: core.serialization.unknown().optional(), + contextId: core.serialization.property("context_id", ContextId.optional()), +}); + +export declare namespace WebSocketTtsOutput { + interface Raw { + word_timestamps?: WordTimestamps.Raw | null; + audio?: unknown | null; + context_id?: ContextId.Raw | null; + } +} diff --git a/src/serialization/resources/tts/types/WebSocketTtsRequest.ts b/src/serialization/resources/tts/types/WebSocketTtsRequest.ts new file mode 100644 index 0000000..ce2e328 --- /dev/null +++ b/src/serialization/resources/tts/types/WebSocketTtsRequest.ts @@ -0,0 +1,36 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { OutputFormat } from "./OutputFormat"; +import { TtsRequestVoiceSpecifier } from "./TtsRequestVoiceSpecifier"; + +export const WebSocketTtsRequest: core.serialization.ObjectSchema< + serializers.WebSocketTtsRequest.Raw, + Cartesia.WebSocketTtsRequest +> = core.serialization.object({ + modelId: core.serialization.property("model_id", core.serialization.string()), + outputFormat: core.serialization.property("output_format", OutputFormat.optional()), + transcript: core.serialization.string().optional(), + voice: TtsRequestVoiceSpecifier, + duration: core.serialization.number().optional(), + language: core.serialization.string().optional(), + addTimestamps: core.serialization.property("add_timestamps", core.serialization.boolean().optional()), + contextId: core.serialization.property("context_id", core.serialization.string().optional()), +}); + +export declare namespace WebSocketTtsRequest { + interface Raw { + model_id: string; + output_format?: OutputFormat.Raw | null; + transcript?: string | null; + voice: TtsRequestVoiceSpecifier.Raw; + duration?: number | null; + language?: string | null; + add_timestamps?: boolean | null; + context_id?: string | null; + } +} diff --git a/src/serialization/resources/tts/types/WordTimestamps.ts b/src/serialization/resources/tts/types/WordTimestamps.ts new file mode 100644 index 0000000..8f50ffe --- /dev/null +++ b/src/serialization/resources/tts/types/WordTimestamps.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const WordTimestamps: core.serialization.ObjectSchema = + core.serialization.object({ + words: core.serialization.list(core.serialization.string()), + start: core.serialization.list(core.serialization.number()), + end: core.serialization.list(core.serialization.number()), + }); + +export declare namespace WordTimestamps { + interface Raw { + words: string[]; + start: number[]; + end: number[]; + } +} diff --git a/src/serialization/resources/tts/types/index.ts b/src/serialization/resources/tts/types/index.ts new file mode 100644 index 0000000..5212040 --- /dev/null +++ b/src/serialization/resources/tts/types/index.ts @@ -0,0 +1,30 @@ +export * from "./ContextId"; +export * from "./WebSocketBaseResponse"; +export * from "./WebSocketResponse"; +export * from "./WebSocketErrorResponse"; +export * from "./WebSocketChunkResponse"; +export * from "./WebSocketTimestampsResponse"; +export * from "./WebSocketTtsOutput"; +export * from "./WebSocketStreamOptions"; +export * from "./WordTimestamps"; +export * from "./WebSocketDoneResponse"; +export * from "./CancelContextRequest"; +export * from "./GenerationRequest"; +export * from "./WebSocketRawOutputFormat"; +export * from "./WebSocketRequest"; +export * from "./WebSocketTtsRequest"; +export * from "./TtsRequest"; +export * from "./SupportedLanguage"; +export * from "./OutputFormat"; +export * from "./RawOutputFormat"; +export * from "./RawEncoding"; +export * from "./WavOutputFormat"; +export * from "./Mp3OutputFormat"; +export * from "./TtsRequestVoiceSpecifier"; +export * from "./TtsRequestIdSpecifier"; +export * from "./TtsRequestEmbeddingSpecifier"; +export * from "./Controls"; +export * from "./Speed"; +export * from "./NumericalSpecifier"; +export * from "./NaturalSpecifier"; +export * from "./Emotion"; diff --git a/src/serialization/resources/voiceChanger/index.ts b/src/serialization/resources/voiceChanger/index.ts new file mode 100644 index 0000000..eea524d --- /dev/null +++ b/src/serialization/resources/voiceChanger/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/src/serialization/resources/voiceChanger/types/OutputFormatContainer.ts b/src/serialization/resources/voiceChanger/types/OutputFormatContainer.ts new file mode 100644 index 0000000..ad08730 --- /dev/null +++ b/src/serialization/resources/voiceChanger/types/OutputFormatContainer.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const OutputFormatContainer: core.serialization.Schema< + serializers.OutputFormatContainer.Raw, + Cartesia.OutputFormatContainer +> = core.serialization.enum_(["raw", "wav", "mp3"]); + +export declare namespace OutputFormatContainer { + type Raw = "raw" | "wav" | "mp3"; +} diff --git a/src/serialization/resources/voiceChanger/types/StreamingResponse.ts b/src/serialization/resources/voiceChanger/types/StreamingResponse.ts new file mode 100644 index 0000000..0629aad --- /dev/null +++ b/src/serialization/resources/voiceChanger/types/StreamingResponse.ts @@ -0,0 +1,40 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { WebSocketChunkResponse } from "../../tts/types/WebSocketChunkResponse"; +import { WebSocketDoneResponse } from "../../tts/types/WebSocketDoneResponse"; +import { WebSocketErrorResponse } from "../../tts/types/WebSocketErrorResponse"; + +export const StreamingResponse: core.serialization.Schema< + serializers.StreamingResponse.Raw, + Cartesia.StreamingResponse +> = core.serialization + .union("type", { + chunk: WebSocketChunkResponse, + done: WebSocketDoneResponse, + error: WebSocketErrorResponse, + }) + .transform({ + transform: (value) => value, + untransform: (value) => value, + }); + +export declare namespace StreamingResponse { + type Raw = StreamingResponse.Chunk | StreamingResponse.Done | StreamingResponse.Error; + + interface Chunk extends WebSocketChunkResponse.Raw { + type: "chunk"; + } + + interface Done extends WebSocketDoneResponse.Raw { + type: "done"; + } + + interface Error extends WebSocketErrorResponse.Raw { + type: "error"; + } +} diff --git a/src/serialization/resources/voiceChanger/types/index.ts b/src/serialization/resources/voiceChanger/types/index.ts new file mode 100644 index 0000000..bc81bbd --- /dev/null +++ b/src/serialization/resources/voiceChanger/types/index.ts @@ -0,0 +1,2 @@ +export * from "./OutputFormatContainer"; +export * from "./StreamingResponse"; diff --git a/src/serialization/resources/voices/client/index.ts b/src/serialization/resources/voices/client/index.ts new file mode 100644 index 0000000..abbe30a --- /dev/null +++ b/src/serialization/resources/voices/client/index.ts @@ -0,0 +1 @@ +export * as list from "./list"; diff --git a/src/serialization/resources/voices/client/list.ts b/src/serialization/resources/voices/client/list.ts new file mode 100644 index 0000000..4cd4a34 --- /dev/null +++ b/src/serialization/resources/voices/client/list.ts @@ -0,0 +1,15 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { Voice } from "../types/Voice"; + +export const Response: core.serialization.Schema = + core.serialization.list(Voice); + +export declare namespace Response { + type Raw = Voice.Raw[]; +} diff --git a/src/serialization/resources/voices/index.ts b/src/serialization/resources/voices/index.ts new file mode 100644 index 0000000..c9240f8 --- /dev/null +++ b/src/serialization/resources/voices/index.ts @@ -0,0 +1,2 @@ +export * from "./types"; +export * from "./client"; diff --git a/src/serialization/resources/voices/types/BaseVoiceId.ts b/src/serialization/resources/voices/types/BaseVoiceId.ts new file mode 100644 index 0000000..d4c5dd7 --- /dev/null +++ b/src/serialization/resources/voices/types/BaseVoiceId.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { VoiceId } from "./VoiceId"; + +export const BaseVoiceId: core.serialization.Schema = VoiceId; + +export declare namespace BaseVoiceId { + type Raw = VoiceId.Raw; +} diff --git a/src/serialization/resources/voices/types/CreateVoiceRequest.ts b/src/serialization/resources/voices/types/CreateVoiceRequest.ts new file mode 100644 index 0000000..3808006 --- /dev/null +++ b/src/serialization/resources/voices/types/CreateVoiceRequest.ts @@ -0,0 +1,31 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { Embedding } from "../../embedding/types/Embedding"; +import { SupportedLanguage } from "../../tts/types/SupportedLanguage"; +import { BaseVoiceId } from "./BaseVoiceId"; + +export const CreateVoiceRequest: core.serialization.ObjectSchema< + serializers.CreateVoiceRequest.Raw, + Cartesia.CreateVoiceRequest +> = core.serialization.object({ + name: core.serialization.string(), + description: core.serialization.string(), + embedding: Embedding, + language: SupportedLanguage.optional(), + baseVoiceId: core.serialization.property("base_voice_id", BaseVoiceId.optional()), +}); + +export declare namespace CreateVoiceRequest { + interface Raw { + name: string; + description: string; + embedding: Embedding.Raw; + language?: SupportedLanguage.Raw | null; + base_voice_id?: BaseVoiceId.Raw | null; + } +} diff --git a/src/serialization/resources/voices/types/EmbeddingResponse.ts b/src/serialization/resources/voices/types/EmbeddingResponse.ts new file mode 100644 index 0000000..e4470df --- /dev/null +++ b/src/serialization/resources/voices/types/EmbeddingResponse.ts @@ -0,0 +1,21 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { Embedding } from "../../embedding/types/Embedding"; + +export const EmbeddingResponse: core.serialization.ObjectSchema< + serializers.EmbeddingResponse.Raw, + Cartesia.EmbeddingResponse +> = core.serialization.object({ + embedding: Embedding, +}); + +export declare namespace EmbeddingResponse { + interface Raw { + embedding: Embedding.Raw; + } +} diff --git a/src/serialization/resources/voices/types/EmbeddingSpecifier.ts b/src/serialization/resources/voices/types/EmbeddingSpecifier.ts new file mode 100644 index 0000000..443c4f0 --- /dev/null +++ b/src/serialization/resources/voices/types/EmbeddingSpecifier.ts @@ -0,0 +1,24 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { Embedding } from "../../embedding/types/Embedding"; +import { Weight } from "./Weight"; + +export const EmbeddingSpecifier: core.serialization.ObjectSchema< + serializers.EmbeddingSpecifier.Raw, + Cartesia.EmbeddingSpecifier +> = core.serialization.object({ + embedding: Embedding, + weight: Weight, +}); + +export declare namespace EmbeddingSpecifier { + interface Raw { + embedding: Embedding.Raw; + weight: Weight.Raw; + } +} diff --git a/src/serialization/resources/voices/types/Gender.ts b/src/serialization/resources/voices/types/Gender.ts new file mode 100644 index 0000000..68d24f1 --- /dev/null +++ b/src/serialization/resources/voices/types/Gender.ts @@ -0,0 +1,16 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const Gender: core.serialization.Schema = core.serialization.enum_([ + "male", + "female", +]); + +export declare namespace Gender { + type Raw = "male" | "female"; +} diff --git a/src/serialization/resources/voices/types/IdSpecifier.ts b/src/serialization/resources/voices/types/IdSpecifier.ts new file mode 100644 index 0000000..bd487ed --- /dev/null +++ b/src/serialization/resources/voices/types/IdSpecifier.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { VoiceId } from "./VoiceId"; +import { Weight } from "./Weight"; + +export const IdSpecifier: core.serialization.ObjectSchema = + core.serialization.object({ + id: VoiceId, + weight: Weight, + }); + +export declare namespace IdSpecifier { + interface Raw { + id: VoiceId.Raw; + weight: Weight.Raw; + } +} diff --git a/src/serialization/resources/voices/types/LocalizeDialect.ts b/src/serialization/resources/voices/types/LocalizeDialect.ts new file mode 100644 index 0000000..8391ee4 --- /dev/null +++ b/src/serialization/resources/voices/types/LocalizeDialect.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const LocalizeDialect: core.serialization.Schema = + core.serialization.enum_(["au", "in", "so", "uk", "us"]); + +export declare namespace LocalizeDialect { + type Raw = "au" | "in" | "so" | "uk" | "us"; +} diff --git a/src/serialization/resources/voices/types/LocalizeTargetLanguage.ts b/src/serialization/resources/voices/types/LocalizeTargetLanguage.ts new file mode 100644 index 0000000..4c3c868 --- /dev/null +++ b/src/serialization/resources/voices/types/LocalizeTargetLanguage.ts @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const LocalizeTargetLanguage: core.serialization.Schema< + serializers.LocalizeTargetLanguage.Raw, + Cartesia.LocalizeTargetLanguage +> = core.serialization.enum_([ + "en", + "de", + "es", + "fr", + "ja", + "pt", + "zh", + "hi", + "it", + "ko", + "nl", + "pl", + "ru", + "sv", + "tr", +]); + +export declare namespace LocalizeTargetLanguage { + type Raw = "en" | "de" | "es" | "fr" | "ja" | "pt" | "zh" | "hi" | "it" | "ko" | "nl" | "pl" | "ru" | "sv" | "tr"; +} diff --git a/src/serialization/resources/voices/types/LocalizeVoiceRequest.ts b/src/serialization/resources/voices/types/LocalizeVoiceRequest.ts new file mode 100644 index 0000000..1f08fb9 --- /dev/null +++ b/src/serialization/resources/voices/types/LocalizeVoiceRequest.ts @@ -0,0 +1,30 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { Embedding } from "../../embedding/types/Embedding"; +import { LocalizeTargetLanguage } from "./LocalizeTargetLanguage"; +import { Gender } from "./Gender"; +import { LocalizeDialect } from "./LocalizeDialect"; + +export const LocalizeVoiceRequest: core.serialization.ObjectSchema< + serializers.LocalizeVoiceRequest.Raw, + Cartesia.LocalizeVoiceRequest +> = core.serialization.object({ + embedding: Embedding, + language: LocalizeTargetLanguage, + originalSpeakerGender: core.serialization.property("original_speaker_gender", Gender), + dialect: LocalizeDialect.optional(), +}); + +export declare namespace LocalizeVoiceRequest { + interface Raw { + embedding: Embedding.Raw; + language: LocalizeTargetLanguage.Raw; + original_speaker_gender: Gender.Raw; + dialect?: LocalizeDialect.Raw | null; + } +} diff --git a/src/serialization/resources/voices/types/MixVoiceSpecifier.ts b/src/serialization/resources/voices/types/MixVoiceSpecifier.ts new file mode 100644 index 0000000..594de8a --- /dev/null +++ b/src/serialization/resources/voices/types/MixVoiceSpecifier.ts @@ -0,0 +1,18 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { IdSpecifier } from "./IdSpecifier"; +import { EmbeddingSpecifier } from "./EmbeddingSpecifier"; + +export const MixVoiceSpecifier: core.serialization.Schema< + serializers.MixVoiceSpecifier.Raw, + Cartesia.MixVoiceSpecifier +> = core.serialization.undiscriminatedUnion([IdSpecifier, EmbeddingSpecifier]); + +export declare namespace MixVoiceSpecifier { + type Raw = IdSpecifier.Raw | EmbeddingSpecifier.Raw; +} diff --git a/src/serialization/resources/voices/types/MixVoicesRequest.ts b/src/serialization/resources/voices/types/MixVoicesRequest.ts new file mode 100644 index 0000000..e1c9780 --- /dev/null +++ b/src/serialization/resources/voices/types/MixVoicesRequest.ts @@ -0,0 +1,21 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { MixVoiceSpecifier } from "./MixVoiceSpecifier"; + +export const MixVoicesRequest: core.serialization.ObjectSchema< + serializers.MixVoicesRequest.Raw, + Cartesia.MixVoicesRequest +> = core.serialization.object({ + voices: core.serialization.list(MixVoiceSpecifier), +}); + +export declare namespace MixVoicesRequest { + interface Raw { + voices: MixVoiceSpecifier.Raw[]; + } +} diff --git a/src/serialization/resources/voices/types/UpdateVoiceRequest.ts b/src/serialization/resources/voices/types/UpdateVoiceRequest.ts new file mode 100644 index 0000000..4da8115 --- /dev/null +++ b/src/serialization/resources/voices/types/UpdateVoiceRequest.ts @@ -0,0 +1,22 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const UpdateVoiceRequest: core.serialization.ObjectSchema< + serializers.UpdateVoiceRequest.Raw, + Cartesia.UpdateVoiceRequest +> = core.serialization.object({ + name: core.serialization.string(), + description: core.serialization.string(), +}); + +export declare namespace UpdateVoiceRequest { + interface Raw { + name: string; + description: string; + } +} diff --git a/src/serialization/resources/voices/types/Voice.ts b/src/serialization/resources/voices/types/Voice.ts new file mode 100644 index 0000000..185c729 --- /dev/null +++ b/src/serialization/resources/voices/types/Voice.ts @@ -0,0 +1,37 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; +import { VoiceId } from "./VoiceId"; +import { Embedding } from "../../embedding/types/Embedding"; +import { SupportedLanguage } from "../../tts/types/SupportedLanguage"; +import { BaseVoiceId } from "./BaseVoiceId"; + +export const Voice: core.serialization.ObjectSchema = core.serialization.object({ + id: VoiceId, + userId: core.serialization.property("user_id", core.serialization.string().optional()), + isPublic: core.serialization.property("is_public", core.serialization.boolean()), + name: core.serialization.string(), + description: core.serialization.string(), + createdAt: core.serialization.property("created_at", core.serialization.date()), + embedding: Embedding, + language: SupportedLanguage, + baseVoiceId: core.serialization.property("base_voice_id", BaseVoiceId.optional()), +}); + +export declare namespace Voice { + interface Raw { + id: VoiceId.Raw; + user_id?: string | null; + is_public: boolean; + name: string; + description: string; + created_at: string; + embedding: Embedding.Raw; + language: SupportedLanguage.Raw; + base_voice_id?: BaseVoiceId.Raw | null; + } +} diff --git a/src/serialization/resources/voices/types/VoiceId.ts b/src/serialization/resources/voices/types/VoiceId.ts new file mode 100644 index 0000000..836e6bd --- /dev/null +++ b/src/serialization/resources/voices/types/VoiceId.ts @@ -0,0 +1,14 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const VoiceId: core.serialization.Schema = + core.serialization.string(); + +export declare namespace VoiceId { + type Raw = string; +} diff --git a/src/serialization/resources/voices/types/Weight.ts b/src/serialization/resources/voices/types/Weight.ts new file mode 100644 index 0000000..1a46618 --- /dev/null +++ b/src/serialization/resources/voices/types/Weight.ts @@ -0,0 +1,13 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ + +import * as serializers from "../../../index"; +import * as Cartesia from "../../../../api/index"; +import * as core from "../../../../core"; + +export const Weight: core.serialization.Schema = core.serialization.number(); + +export declare namespace Weight { + type Raw = number; +} diff --git a/src/serialization/resources/voices/types/index.ts b/src/serialization/resources/voices/types/index.ts new file mode 100644 index 0000000..35b54d7 --- /dev/null +++ b/src/serialization/resources/voices/types/index.ts @@ -0,0 +1,15 @@ +export * from "./VoiceId"; +export * from "./BaseVoiceId"; +export * from "./Voice"; +export * from "./CreateVoiceRequest"; +export * from "./UpdateVoiceRequest"; +export * from "./LocalizeTargetLanguage"; +export * from "./LocalizeDialect"; +export * from "./Gender"; +export * from "./LocalizeVoiceRequest"; +export * from "./EmbeddingResponse"; +export * from "./MixVoicesRequest"; +export * from "./Weight"; +export * from "./IdSpecifier"; +export * from "./EmbeddingSpecifier"; +export * from "./MixVoiceSpecifier"; diff --git a/src/wrapper/Client.ts b/src/wrapper/Client.ts new file mode 100644 index 0000000..9a2db58 --- /dev/null +++ b/src/wrapper/Client.ts @@ -0,0 +1,11 @@ +import { CartesiaClient as FernCartesiaClient } from "../Client"; +import { StreamingTTSClient } from "./StreamingTTSClient"; + +export class CartesiaClient extends FernCartesiaClient { + + protected _tts: StreamingTTSClient | undefined; + + public get tts(): StreamingTTSClient { + return (this._tts ??= new StreamingTTSClient(this._options)); + } +} diff --git a/src/wrapper/StreamingTTSClient.ts b/src/wrapper/StreamingTTSClient.ts new file mode 100644 index 0000000..0021131 --- /dev/null +++ b/src/wrapper/StreamingTTSClient.ts @@ -0,0 +1,23 @@ +import { Tts } from "../api/resources/tts/client/Client"; +import Websocket from "./Websocket"; + + +export class StreamingTTSClient extends Tts { + constructor(options: Tts.Options = {}) { + super(options); + } + + /** + * Get a WebSocket client for streaming TTS. + * + * @param options - Options for the WebSocket client. + * @returns A WebSocket client configured for streaming TTS. + */ + websocket({ sampleRate, container, encoding }: { + sampleRate: number; + container?: string; + encoding?: string + }): Websocket { + return new Websocket({ sampleRate, container, encoding }, {cartesiaVersion: "2024-06-10", ...this._options}); + } +} diff --git a/src/wrapper/Websocket.ts b/src/wrapper/Websocket.ts new file mode 100644 index 0000000..d4c5961 --- /dev/null +++ b/src/wrapper/Websocket.ts @@ -0,0 +1,252 @@ +import * as core from "../core"; +import * as environments from "../environments"; +import * as serializers from "../serialization"; +import Emittery from "emittery"; +import { humanId } from "human-id"; +import { ReconnectingWebSocket, Options } from "../core/websocket"; +import type { + RawEncoding, + WebSocketStreamOptions, + WebSocketTtsRequest, + WordTimestamps, +} from "../api"; +import { + base64ToArray, + resolveOutputFormat, + ConnectionEventData, + createMessageHandlerForContextId, + getEmitteryCallbacks, + isSentinel, + WebSocketOptions, + EmitteryCallbacks, +} from "./utils"; +import { Tts } from "api/resources/tts/client/Client"; +import Source from "./source"; +import qs from "qs"; + +export default class Websocket { + socket?: ReconnectingWebSocket; + #isConnected = false; + #sampleRate: number; + #container: string; + #encoding: string; + + constructor( + { sampleRate, container, encoding }: WebSocketOptions, + private readonly options: Tts.Options + ) { + this.#sampleRate = sampleRate; + this.#container = container ?? "raw"; + this.#encoding = encoding ?? "pcm_f32le"; + } + + /** + * Send a message over the WebSocket to start a stream. + * + * @param inputs - Generation parameters. Defined in the StreamRequest type. + * @param options - Options for the stream. + * @param options.timeout - The maximum time to wait for a chunk before cancelling the stream. + * If set to `0`, the stream will not time out. + * @returns A Source object that can be passed to a Player to play the audio. + * @returns An Emittery instance that emits messages from the WebSocket. + * @returns An abort function that can be called to cancel the stream. + */ + send(inputs: WebSocketTtsRequest, { timeout = 0 }: WebSocketStreamOptions = {}) { + if (!this.#isConnected) { + throw new Error("Not connected to WebSocket. Call .connect() first."); + } + + if (!inputs.contextId) { + inputs.contextId = this.#generateId(); + } + if (!inputs.outputFormat) { + inputs.outputFormat = resolveOutputFormat( + this.#container as "raw" | "wav" | "mp3", + this.#encoding as RawEncoding, + this.#sampleRate, + ) + } + + this.socket?.send(JSON.stringify(serializers.WebSocketTtsRequest.jsonOrThrow(inputs, { unrecognizedObjectKeys: "strip" }))); + + const emitter = new Emittery<{ + message: string; + timestamps: WordTimestamps; + }>(); + const source = new Source({ + sampleRate: this.#sampleRate, + encoding: this.#encoding, + container: this.#container, + }); + // Used to signal that the stream is complete, either because the + // WebSocket has closed, or because the stream has finished. + const streamCompleteController = new AbortController(); + // Set a timeout. + let timeoutId: ReturnType | null = null; + if (timeout > 0) { + timeoutId = setTimeout(streamCompleteController.abort, timeout); + } + const handleMessage = createMessageHandlerForContextId( + inputs.contextId, + async ({ chunk, message, data }) => { + emitter.emit("message", message); + if (data.type === "timestamps" && data.wordTimestamps) { + emitter.emit("timestamps", data.wordTimestamps); + return; + } + if (isSentinel(chunk)) { + await source.close(); + streamCompleteController.abort(); + return; + } + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = setTimeout(streamCompleteController.abort, timeout); + } + if (!chunk) { + return; + } + await source.enqueue(base64ToArray([chunk], this.#encoding)); + }, + ); + this.socket?.addEventListener("message", handleMessage); + this.socket?.addEventListener( + "close", + () => { + streamCompleteController.abort(); + } + ); + this.socket?.addEventListener( + "error", + () => { + streamCompleteController.abort(); + } + ); + streamCompleteController.signal.addEventListener("abort", () => { + source.close(); + if (timeoutId) { + clearTimeout(timeoutId); + } + emitter.clearListeners(); + }); + + return { + source, + ...getEmitteryCallbacks(emitter), + stop: streamCompleteController.abort.bind(streamCompleteController), + }; + } + + + continue(inputs: WebSocketTtsRequest) { + if (!this.#isConnected) { + throw new Error("Not connected to WebSocket. Call .connect() first."); + } + + if (!inputs.contextId) { + throw new Error("context_id is required to continue a context."); + } + if (!inputs.outputFormat) { + inputs.outputFormat = resolveOutputFormat( + this.#container as "raw" | "wav" | "mp3", + this.#encoding as RawEncoding, + this.#sampleRate, + ) + } + + this.socket?.send( + JSON.stringify({ + continue: true, + ...serializers.WebSocketTtsRequest.jsonOrThrow(inputs, { unrecognizedObjectKeys: "strip" }), + }), + ); + } + + /** + * Generate a unique ID suitable for a streaming context. + * + * Not suitable for security purposes or as a primary key, since + * it lacks the amount of entropy required for those use cases. + * + * @returns A unique ID. + */ + #generateId() { + return humanId({ + separator: "-", + capitalize: false, + }); + } + + /** + * Authenticate and connect to a Cartesia streaming WebSocket. + * + * @returns A promise that resolves when the WebSocket is connected. + * @throws {Error} If the WebSocket fails to connect. + */ + async connect(options: Options = {}) { + if (this.#isConnected) { + throw new Error("WebSocket is already connected."); + } + + const emitter = new Emittery(); + this.socket = new ReconnectingWebSocket( + async () => { + const baseUrl = + (await core.Supplier.get(this.options.environment) ?? + environments.CartesiaEnvironment.Production).replace(/^https?:\/\//, ""); + const params = { + api_key: this.options.apiKeyHeader, + cartesia_version: this.options.cartesiaVersion, + }; + return `wss://${baseUrl}/tts/websocket${qs.stringify(params, { addQueryPrefix: true })}`; + }, + undefined, + options, + ); + this.socket!.reconnect(); + + this.socket!.onopen = () => { + this.#isConnected = true; + emitter.emit("open"); + }; + this.socket!.onclose = () => { + this.#isConnected = false; + emitter.emit("close"); + }; + + return new Promise>( + (resolve, reject) => { + this.socket?.addEventListener( + "open", + () => { + resolve(getEmitteryCallbacks(emitter)); + } + ); + + const aborter = new AbortController(); + this.socket?.addEventListener( + "error", + () => { + aborter.abort(); + reject(new Error("WebSocket failed to connect.")); + } + ); + + this.socket?.addEventListener( + "close", + () => { + aborter.abort(); + reject(new Error("WebSocket closed before it could connect.")); + } + ); + }, + ); + } + + /** + * Disconnect from the Cartesia streaming WebSocket. + */ + disconnect() { + this.socket?.close(); + } +} diff --git a/src/wrapper/source.ts b/src/wrapper/source.ts new file mode 100644 index 0000000..7b9fc13 --- /dev/null +++ b/src/wrapper/source.ts @@ -0,0 +1,182 @@ +import Emittery from "emittery"; +import { RawEncoding } from "../api"; +import { ENCODING_MAP, SourceEventData, TypedArray } from "./utils"; + +export default class Source { + #emitter = new Emittery(); + #buffer: TypedArray; + #readIndex = 0; + #writeIndex = 0; + #closed = false; + #sampleRate: number; + #encoding: RawEncoding; + #container: string; + + on = this.#emitter.on.bind(this.#emitter); + once = this.#emitter.once.bind(this.#emitter); + events = this.#emitter.events.bind(this.#emitter); + off = this.#emitter.off.bind(this.#emitter); + + /** + * Create a new Source. + * + * @param options - Options for the Source. + * @param options.sampleRate - The sample rate of the audio. + */ + constructor({ + sampleRate, + encoding, + container, + }: { sampleRate: number; encoding: string; container: string }) { + this.#sampleRate = sampleRate; + this.#encoding = encoding as RawEncoding; + this.#container = container; + this.#buffer = this.#createBuffer(1024); // Initial size, can be adjusted + } + + get sampleRate() { + return this.#sampleRate; + } + + get encoding() { + return this.#encoding; + } + + get container() { + return this.#container; + } + + /** + * Create a new buffer for the source. + * + * @param size - The size of the buffer to create. + * @returns The new buffer as a TypedArray based on the encoding. + */ + #createBuffer(size: number): TypedArray { + const { arrayType: ArrayType } = ENCODING_MAP[this.#encoding]; + return new ArrayType(size); + } + + /** + * Append audio to the buffer. + * + * @param src The audio to append. + */ + async enqueue(src: TypedArray) { + const requiredCapacity = this.#writeIndex + src.length; + + // Resize buffer if necessary + if (requiredCapacity > this.#buffer.length) { + let newCapacity = this.#buffer.length; + while (newCapacity < requiredCapacity) { + newCapacity *= 2; // Double the buffer size + } + + const newBuffer = this.#createBuffer(newCapacity); + newBuffer.set(this.#buffer); + this.#buffer = newBuffer; + } + + // Append the audio to the buffer. + this.#buffer.set(src, this.#writeIndex); + this.#writeIndex += src.length; + await this.#emitter.emit("enqueue"); + } + + /** + * Read audio from the buffer. + * + * @param dst The buffer to read the audio into. + * @returns The number of samples read. If the source is closed, this will be + * less than the length of the provided buffer. + */ + async read(dst: TypedArray): Promise { + // Read the buffer into the provided buffer. + const targetReadIndex = this.#readIndex + dst.length; + + while (!this.#closed && targetReadIndex > this.#writeIndex) { + // Wait for more audio to be enqueued. + await this.#emitter.emit("wait"); + await Promise.race([ + this.#emitter.once("enqueue"), + this.#emitter.once("close"), + ]); + await this.#emitter.emit("read"); + } + + const read = Math.min(dst.length, this.#writeIndex - this.#readIndex); + dst.set(this.#buffer.subarray(this.#readIndex, this.#readIndex + read)); + this.#readIndex += read; + return read; + } + + /** + * Seek in the buffer. + * + * @param offset The offset to seek to. + * @param whence The position to seek from. + * @returns The new position in the buffer. + * @throws {Error} If the seek is invalid. + */ + async seek( + offset: number, + whence: "start" | "current" | "end", + ): Promise { + let position = this.#readIndex; + switch (whence) { + case "start": + position = offset; + break; + case "current": + position += offset; + break; + case "end": + position = this.#writeIndex + offset; + break; + default: + throw new Error(`Invalid seek mode: ${whence}`); + } + + if (position < 0 || position > this.#writeIndex) { + throw new Error("Seek out of bounds"); + } + + this.#readIndex = position; + return position; + } + + /** + * Get the number of samples in a given duration. + * + * @param durationSecs The duration in seconds. + * @returns The number of samples. + */ + durationToSampleCount(durationSecs: number) { + return Math.trunc(durationSecs * this.#sampleRate); + } + + get buffer() { + return this.#buffer; + } + + get readIndex() { + return this.#readIndex; + } + + get writeIndex() { + return this.#writeIndex; + } + + /** + * Close the source. This signals that no more audio will be enqueued. + * + * This will emit a "close" event. + * + * @returns A promise that resolves when the source is closed. + */ + async close() { + this.#closed = true; + await this.#emitter.emit("close"); + this.#emitter.clearListeners(); + } +} \ No newline at end of file diff --git a/src/wrapper/utils.ts b/src/wrapper/utils.ts new file mode 100644 index 0000000..ae48b68 --- /dev/null +++ b/src/wrapper/utils.ts @@ -0,0 +1,251 @@ +import base64 from "base64-js"; +import type Emittery from "emittery"; +import type { OutputFormat, RawEncoding, WebSocketResponse, WebSocketTtsRequest } from "../api"; + +export type EmitteryCallbacks = { + on: Emittery["on"]; + off: Emittery["off"]; + once: Emittery["once"]; + events: Emittery["events"]; +}; + +export type ConnectionEventData = { + open: never; + close: never; +}; + +export type SourceEventData = { + enqueue: never; + close: never; + wait: never; + read: never; +}; + +export type TypedArray = Float32Array | Int16Array | Uint8Array; + +export type Sentinel = null; + +export type Chunk = string | Sentinel; + +export type WebSocketOptions = { + container?: string; + encoding?: string; + sampleRate: number; +}; + +export type Language = + | "en" + | "es" + | "fr" + | "de" + | "ja" + | "zh" + | "pt" + | (string & {}); + +export type EncodingInfo = { + arrayType: + | Float32ArrayConstructor + | Int16ArrayConstructor + | Uint8ArrayConstructor; + bytesPerElement: number; +}; + +export const ENCODING_MAP: Record = { + pcm_f32le: { arrayType: Float32Array, bytesPerElement: 4 }, + pcm_s16le: { arrayType: Int16Array, bytesPerElement: 2 }, + pcm_alaw: { arrayType: Uint8Array, bytesPerElement: 1 }, + pcm_mulaw: { arrayType: Uint8Array, bytesPerElement: 1 }, +}; + +/** +* Resolve the output format for a WebSocket request. +* +* @param container - The container type for the output. +* @param encoding - The encoding of the audio. +* @param sampleRate - The sample rate of the audio. +* @returns The output format for the WebSocket request. +*/ +export function resolveOutputFormat( + container: "raw" | "wav" | "mp3", + encoding: RawEncoding, + sampleRate: number +): OutputFormat { + switch (container) { + case "wav": + return { + container: "wav", + encoding, + sampleRate, + } as OutputFormat.Wav; + case "raw": + return { + container: "raw", + encoding, + sampleRate, + } as OutputFormat.Raw; + case "mp3": + return { + container: "mp3", + encoding, + sampleRate, + bitRate: 128, + } as OutputFormat.Mp3; + default: + throw new Error(`Unsupported container type: ${container}`); + } +} + + +/** + * Convert base64-encoded audio buffer(s) to a TypedArray. + * + * @param b64 The base64-encoded audio buffer, or an array of base64-encoded + * audio buffers. + * @param encoding The encoding of the audio buffer(s). + * @returns The audio buffer(s) as a TypedArray. + */ +export function base64ToArray(b64: Chunk[], encoding: string): TypedArray { + const byteArrays = filterSentinel(b64).map((b) => base64.toByteArray(b)); + + const { arrayType: ArrayType, bytesPerElement } = + ENCODING_MAP[encoding as RawEncoding]; + + const totalLength = byteArrays.reduce( + (acc, arr) => acc + arr.length / bytesPerElement, + 0, + ); + const result = new ArrayType(totalLength); + + let offset = 0; + for (const arr of byteArrays) { + const floats = new ArrayType(arr.buffer); + result.set(floats, offset); + offset += floats.length; + } + + return result; +} + +/** + * Schedule an audio buffer to play at a given time in the passed context. + * + * @param floats The audio buffer to play. + * @param context The audio context to play the buffer in. + * @param startAt The time to start playing the buffer at. + * @param sampleRate The sample rate of the audio. + * @returns A promise that resolves when the audio has finished playing. + */ +export function playAudioBuffer( + floats: Float32Array, + context: AudioContext, + startAt: number, + sampleRate: number, +) { + const source = context.createBufferSource(); + const buffer = context.createBuffer(1, floats.length, sampleRate); + buffer.getChannelData(0).set(floats); + source.buffer = buffer; + source.connect(context.destination); + source.start(startAt); + + return new Promise((resolve) => { + source.onended = () => { + resolve(); + }; + }); +} + +/** + * Unwraps a chunk of audio data from a message event and calls the + * handler with it if the context ID matches. + * + * @param contextId The context ID to listen for. + * @param handler The handler to call with the chunk of audio data. + * @returns A message event handler. + */ +export function createMessageHandlerForContextId( + contextId: string, + handler: ({ + chunk, + message, + }: { + chunk?: Chunk; + message: string; + data: WebSocketResponse; + }) => void, +) { + return (event: MessageEvent) => { + if (typeof event.data !== "string") { + return; // Ignore non-string messages. + } + const message = JSON.parse(event.data); + if (message.context_id !== contextId) { + return; // Ignore messages for other contexts. + } + let chunk: Chunk | undefined; + if (message.done) { + // Convert the done message to a sentinel value. + chunk = getSentinel(); + } else if (message.type === "chunk") { + chunk = message.data; + } + handler({ chunk, message: event.data, data: message }); + }; +} + +/** + * Get a sentinel value that indicates the end of a stream. + * @returns A sentinel value to indicate the end of a stream. + */ +export function getSentinel(): Sentinel { + return null; +} + +/** + * Check if a chunk is a sentinel value (i.e. null). + * + * @param chunk + * @returns Whether the chunk is a sentinel value. + */ +export function isSentinel(x: unknown): x is Sentinel { + return x === getSentinel(); +} + +/** + * Filter out null values from a collection. + * + * @param collection The collection to filter. + * @returns The collection with null values removed. + */ +export function filterSentinel(collection: T[]): Exclude[] { + return collection.filter( + (x): x is Exclude> => !isSentinel(x), + ); +} + +/** + * Check if an array of chunks is complete by testing if the last chunk is a sentinel + * value (i.e. null). + * @param chunk + * @returns Whether the array of chunks is complete. + */ +export function isComplete(chunks: Chunk[]) { + return isSentinel(chunks[chunks.length - 1]); +} + +/** + * Get user-facing emitter callbacks for an Emittery instance. + * @param emitter The Emittery instance to get callbacks for. + * @returns User-facing emitter callbacks. + */ +export function getEmitteryCallbacks( + emitter: Emittery, +): EmitteryCallbacks { + return { + on: emitter.on.bind(emitter), + off: emitter.off.bind(emitter), + once: emitter.once.bind(emitter), + events: emitter.events.bind(emitter), + }; +} \ No newline at end of file diff --git a/tests/unit/zurg/bigint/bigint.test.ts b/tests/unit/zurg/bigint/bigint.test.ts new file mode 100644 index 0000000..cf9935a --- /dev/null +++ b/tests/unit/zurg/bigint/bigint.test.ts @@ -0,0 +1,24 @@ +import { bigint } from "../../../../src/core/schemas/builders/bigint"; +import { itSchema } from "../utils/itSchema"; +import { itValidateJson, itValidateParse } from "../utils/itValidate"; + +describe("bigint", () => { + itSchema("converts between raw string and parsed bigint", bigint(), { + raw: "123456789012345678901234567890123456789012345678901234567890", + parsed: BigInt("123456789012345678901234567890123456789012345678901234567890"), + }); + + itValidateParse("non-string", bigint(), 42, [ + { + message: "Expected string. Received 42.", + path: [], + }, + ]); + + itValidateJson("non-bigint", bigint(), "hello", [ + { + message: 'Expected bigint. Received "hello".', + path: [], + }, + ]); +}); diff --git a/tests/unit/zurg/date/date.test.ts b/tests/unit/zurg/date/date.test.ts new file mode 100644 index 0000000..2790268 --- /dev/null +++ b/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/tests/unit/zurg/enum/enum.test.ts b/tests/unit/zurg/enum/enum.test.ts new file mode 100644 index 0000000..ab0df02 --- /dev/null +++ b/tests/unit/zurg/enum/enum.test.ts @@ -0,0 +1,30 @@ +import { enum_ } from "../../../../src/core/schemas/builders/enum"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +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/tests/unit/zurg/lazy/lazy.test.ts b/tests/unit/zurg/lazy/lazy.test.ts new file mode 100644 index 0000000..6906bf4 --- /dev/null +++ b/tests/unit/zurg/lazy/lazy.test.ts @@ -0,0 +1,57 @@ +import { Schema } from "../../../../src/core/schemas/Schema"; +import { lazy, list, object, string } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/lazy/lazyObject.test.ts b/tests/unit/zurg/lazy/lazyObject.test.ts new file mode 100644 index 0000000..8813cc9 --- /dev/null +++ b/tests/unit/zurg/lazy/lazyObject.test.ts @@ -0,0 +1,18 @@ +import { lazyObject, number, object, string } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/lazy/recursive/a.ts b/tests/unit/zurg/lazy/recursive/a.ts new file mode 100644 index 0000000..8b7d5e4 --- /dev/null +++ b/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/tests/unit/zurg/lazy/recursive/b.ts b/tests/unit/zurg/lazy/recursive/b.ts new file mode 100644 index 0000000..fb219d5 --- /dev/null +++ b/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/tests/unit/zurg/list/list.test.ts b/tests/unit/zurg/list/list.test.ts new file mode 100644 index 0000000..424ed64 --- /dev/null +++ b/tests/unit/zurg/list/list.test.ts @@ -0,0 +1,41 @@ +import { list, object, property, string } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/literals/stringLiteral.test.ts b/tests/unit/zurg/literals/stringLiteral.test.ts new file mode 100644 index 0000000..fa6c888 --- /dev/null +++ b/tests/unit/zurg/literals/stringLiteral.test.ts @@ -0,0 +1,21 @@ +import { stringLiteral } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +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/tests/unit/zurg/object-like/withParsedProperties.test.ts b/tests/unit/zurg/object-like/withParsedProperties.test.ts new file mode 100644 index 0000000..9f5dd0e --- /dev/null +++ b/tests/unit/zurg/object-like/withParsedProperties.test.ts @@ -0,0 +1,57 @@ +import { object, property, string, stringLiteral } from "../../../../src/core/schemas/builders"; + +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/tests/unit/zurg/object/extend.test.ts b/tests/unit/zurg/object/extend.test.ts new file mode 100644 index 0000000..54fc8c4 --- /dev/null +++ b/tests/unit/zurg/object/extend.test.ts @@ -0,0 +1,89 @@ +import { boolean, object, property, string, stringLiteral } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/object/object.test.ts b/tests/unit/zurg/object/object.test.ts new file mode 100644 index 0000000..0acf0e2 --- /dev/null +++ b/tests/unit/zurg/object/object.test.ts @@ -0,0 +1,255 @@ +import { any, number, object, property, string, stringLiteral, unknown } from "../../../../src/core/schemas/builders"; +import { itJson, itParse, itSchema, itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +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 }, + } + ); + }); + }); + }); + + 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/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts b/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts new file mode 100644 index 0000000..d87a65f --- /dev/null +++ b/tests/unit/zurg/object/objectWithoutOptionalProperties.test.ts @@ -0,0 +1,21 @@ +import { objectWithoutOptionalProperties, string, stringLiteral } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/primitives/any.test.ts b/tests/unit/zurg/primitives/any.test.ts new file mode 100644 index 0000000..1adbbe2 --- /dev/null +++ b/tests/unit/zurg/primitives/any.test.ts @@ -0,0 +1,6 @@ +import { any } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("any", () => { + itSchemaIdentity(any(), true); +}); diff --git a/tests/unit/zurg/primitives/boolean.test.ts b/tests/unit/zurg/primitives/boolean.test.ts new file mode 100644 index 0000000..897a829 --- /dev/null +++ b/tests/unit/zurg/primitives/boolean.test.ts @@ -0,0 +1,14 @@ +import { boolean } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("boolean", () => { + itSchemaIdentity(boolean(), true); + + itValidate("non-boolean", boolean(), {}, [ + { + path: [], + message: "Expected boolean. Received object.", + }, + ]); +}); diff --git a/tests/unit/zurg/primitives/number.test.ts b/tests/unit/zurg/primitives/number.test.ts new file mode 100644 index 0000000..2d01415 --- /dev/null +++ b/tests/unit/zurg/primitives/number.test.ts @@ -0,0 +1,14 @@ +import { number } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("number", () => { + itSchemaIdentity(number(), 42); + + itValidate("non-number", number(), "hello", [ + { + path: [], + message: 'Expected number. Received "hello".', + }, + ]); +}); diff --git a/tests/unit/zurg/primitives/string.test.ts b/tests/unit/zurg/primitives/string.test.ts new file mode 100644 index 0000000..57b2368 --- /dev/null +++ b/tests/unit/zurg/primitives/string.test.ts @@ -0,0 +1,14 @@ +import { string } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; +import { itValidate } from "../utils/itValidate"; + +describe("string", () => { + itSchemaIdentity(string(), "hello"); + + itValidate("non-string", string(), 42, [ + { + path: [], + message: "Expected string. Received 42.", + }, + ]); +}); diff --git a/tests/unit/zurg/primitives/unknown.test.ts b/tests/unit/zurg/primitives/unknown.test.ts new file mode 100644 index 0000000..4d17a7d --- /dev/null +++ b/tests/unit/zurg/primitives/unknown.test.ts @@ -0,0 +1,6 @@ +import { unknown } from "../../../../src/core/schemas/builders"; +import { itSchemaIdentity } from "../utils/itSchema"; + +describe("unknown", () => { + itSchemaIdentity(unknown(), true); +}); diff --git a/tests/unit/zurg/record/record.test.ts b/tests/unit/zurg/record/record.test.ts new file mode 100644 index 0000000..7e4ba39 --- /dev/null +++ b/tests/unit/zurg/record/record.test.ts @@ -0,0 +1,34 @@ +import { number, record, string } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts b/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts new file mode 100644 index 0000000..da10086 --- /dev/null +++ b/tests/unit/zurg/schema-utils/getSchemaUtils.test.ts @@ -0,0 +1,83 @@ +import { object, string } from "../../../../src/core/schemas/builders"; +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"')); + }); + }); + + describe("omitUndefined", () => { + it("serializes undefined as null", async () => { + const value = object({ + a: string().optional(), + b: string().optional(), + }).jsonOrThrow({ + a: "hello", + b: undefined, + }); + expect(value).toEqual({ a: "hello", b: null }); + }); + + it("omits undefined values", async () => { + const value = object({ + a: string().optional(), + b: string().optional(), + }).jsonOrThrow( + { + a: "hello", + b: undefined, + }, + { + omitUndefined: true, + } + ); + expect(value).toEqual({ a: "hello" }); + }); + }); +}); diff --git a/tests/unit/zurg/schema.test.ts b/tests/unit/zurg/schema.test.ts new file mode 100644 index 0000000..94089a9 --- /dev/null +++ b/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/tests/unit/zurg/set/set.test.ts b/tests/unit/zurg/set/set.test.ts new file mode 100644 index 0000000..e17f908 --- /dev/null +++ b/tests/unit/zurg/set/set.test.ts @@ -0,0 +1,48 @@ +import { set, string } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/skipValidation.test.ts b/tests/unit/zurg/skipValidation.test.ts new file mode 100644 index 0000000..5dc8809 --- /dev/null +++ b/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/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts b/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts new file mode 100644 index 0000000..0e66433 --- /dev/null +++ b/tests/unit/zurg/undiscriminated-union/undiscriminatedUnion.test.ts @@ -0,0 +1,44 @@ +import { number, object, property, string, undiscriminatedUnion } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/union/union.test.ts b/tests/unit/zurg/union/union.test.ts new file mode 100644 index 0000000..7901846 --- /dev/null +++ b/tests/unit/zurg/union/union.test.ts @@ -0,0 +1,113 @@ +import { boolean, discriminant, number, object, string, union } from "../../../../src/core/schemas/builders"; +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/tests/unit/zurg/utils/itSchema.ts b/tests/unit/zurg/utils/itSchema.ts new file mode 100644 index 0000000..67b6c92 --- /dev/null +++ b/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/tests/unit/zurg/utils/itValidate.ts b/tests/unit/zurg/utils/itValidate.ts new file mode 100644 index 0000000..75b2c08 --- /dev/null +++ b/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/yarn.lock b/yarn.lock index 76771c1..c5586bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -702,6 +702,13 @@ resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-4.0.1.tgz#4989c97f969464647a8586c7252d97b449cdc045" integrity sha512-wDXw9LEEUHyV+7UWy7U315nrJGJ7p1BzaCxDpEoLr789Dk1WDVMMlf3iBfbG2F8NdWnYyFbtTxUn2ZNbm1Q4LQ== +"@types/ws@^8.5.13": + version "8.5.13" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" + integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -1296,6 +1303,11 @@ emittery@^0.13.1: resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== +emittery@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-1.0.3.tgz#c9d2a9c689870f15251bb13b31c67715c26d69ac" + integrity sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -1647,6 +1659,11 @@ https-proxy-agent@^5.0.1: agent-base "6" debug "4" +human-id@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/human-id/-/human-id-4.1.1.tgz#2801fbd61b9a5c1c9170f332802db6408a39a4b0" + integrity sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg== + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -3129,7 +3146,7 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@^8.11.0: +ws@^8.11.0, ws@^8.14.2: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==