From 0e52295e47afc03fc601a8b2254b9c0b11def3ab Mon Sep 17 00:00:00 2001 From: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:15:33 +0000 Subject: [PATCH 1/5] SDK regeneration --- .fern/metadata.json | 7 +- README.md | 53 +- reference.md | 9 +- src/Hume.Test/Core/Json/DateOnlyJsonTests.cs | 24 + src/Hume.Test/Core/Json/DateTimeJsonTests.cs | 24 + .../Core/Json/StringEnumSerializerTests.cs | 138 -- .../Core/Pagination/GuidCursorTest.cs | 15 +- .../Core/Pagination/HasNextPageOffsetTest.cs | 15 +- .../Core/Pagination/IntOffsetTest.cs | 15 +- .../Core/Pagination/LongOffsetTest.cs | 15 +- .../Core/Pagination/NoRequestCursorTest.cs | 15 +- .../Core/Pagination/NoRequestOffsetTest.cs | 15 +- .../Core/Pagination/StepOffsetTest.cs | 15 +- .../Core/Pagination/StringCursorTest.cs | 17 +- src/Hume.Test/Core/QueryStringBuilderTests.cs | 102 +- .../RawClientTests/QueryParameterTests.cs | 2 +- .../Core/RawClientTests/RetriesTests.cs | 13 +- .../Core/WebSockets/AsyncLockTests.cs | 153 +++ .../Core/WebSockets/DisconnectionInfoTests.cs | 93 ++ .../Core/WebSockets/E2eWebSocketTests.cs | 780 +++++++++++ src/Hume.Test/Core/WebSockets/EventTests.cs | 205 +++ src/Hume.Test/Core/WebSockets/QueryTests.cs | 299 ++++ .../Core/WebSockets/ReconnectStrategyTests.cs | 184 +++ .../Core/WebSockets/ReconnectionInfoTests.cs | 72 + .../Core/WebSockets/RequestMessageTests.cs | 71 + .../Core/WebSockets/TestWebSocketServer.cs | 328 +++++ .../Core/WebSockets/WebSocketClientTests.cs | 601 ++++++++ .../WebSockets/WebSocketConnectionTests.cs | 1212 +++++++++++++++++ .../WebSockets/WebsocketExceptionTests.cs | 75 + src/Hume.Test/Hume.Test.csproj | 14 +- .../Unit/MockServer/BaseMockServerTest.cs | 7 +- .../{ => ChatGroups}/GetAudioTest.cs | 5 +- .../{ => ChatGroups}/GetChatGroupTest.cs | 5 +- .../ListChatGroupEventsTest.cs | 5 +- .../{ => ChatGroups}/ListChatGroupsTest.cs | 5 +- .../GetAudioTest.cs} | 7 +- .../{ => Chats}/ListChatEventsTest.cs | 5 +- .../{ => Chats}/ListChatsTest.cs | 5 +- .../{ => Configs}/CreateConfigTest.cs | 5 +- .../{ => Configs}/CreateConfigVersionTest.cs | 5 +- .../{ => Configs}/DeleteConfigTest.cs | 3 +- .../{ => Configs}/DeleteConfigVersionTest.cs | 3 +- .../{ => Configs}/GetConfigVersionTest.cs | 5 +- .../{ => Configs}/ListConfigVersionsTest.cs | 5 +- .../{ => Configs}/ListConfigsTest.cs | 5 +- .../UpdateConfigDescriptionTest.cs | 5 +- .../{ => Configs}/UpdateConfigNameTest.cs | 3 +- .../{ => ControlPlane}/SendTest.cs | 8 +- .../EmpathicVoice/DeleteToolVersionTest.cs | 25 - .../{ => Prompts}/CreatePromptTest.cs | 5 +- .../{ => Prompts}/CreatePromptVersionTest.cs | 5 +- .../{ => Prompts}/DeletePromptTest.cs | 3 +- .../{ => Prompts}/DeletePromptVersionTest.cs | 3 +- .../{ => Prompts}/GetPromptVersionTest.cs | 5 +- .../{ => Prompts}/ListPromptVersionsTest.cs | 5 +- .../{ => Prompts}/ListPromptsTest.cs | 5 +- .../UpdatePromptDescriptionTest.cs | 5 +- .../{ => Prompts}/UpdatePromptNameTest.cs | 3 +- .../{ => Tools}/CreateToolTest.cs | 5 +- .../{ => Tools}/CreateToolVersionTest.cs | 5 +- .../{ => Tools}/DeleteToolTest.cs | 3 +- .../{ => Tools}/GetToolVersionTest.cs | 5 +- .../{ => Tools}/ListToolVersionsTest.cs | 5 +- .../{ => Tools}/ListToolsTest.cs | 5 +- .../{ => Tools}/UpdateToolDescriptionTest.cs | 5 +- .../{ => Tools}/UpdateToolNameTest.cs | 3 +- .../Batch/GetJobDetailsTest.cs | 3 +- .../Batch/GetJobPredictionsTest.cs | 3 +- .../Batch/ListJobsTest.cs | 3 +- .../Tts/SynthesizeJsonStreamingTest.cs | 1 + .../Unit/MockServer/Tts/SynthesizeJsonTest.cs | 5 +- .../MockServer/Tts/{ => Voices}/CreateTest.cs | 5 +- .../MockServer/Tts/{ => Voices}/DeleteTest.cs | 3 +- .../MockServer/Tts/{ => Voices}/ListTest.cs | 5 +- .../Utils/AdditionalPropertiesComparer.cs | 95 +- src/Hume.Test/Utils/JsonAssert.cs | 10 + src/Hume.Test/Utils/JsonElementComparer.cs | 17 +- src/Hume.Test/Utils/NUnitExtensions.cs | 4 +- src/Hume/Core/BaseRequest.cs | 2 +- src/Hume/Core/CollectionItemSerializer.cs | 8 +- src/Hume/Core/DateTimeSerializer.cs | 18 + src/Hume/Core/HeaderValue.cs | 41 +- src/Hume/Core/HttpContentExtensions.cs | 20 + src/Hume/Core/IRequestOptions.cs | 2 +- src/Hume/Core/JsonConfiguration.cs | 4 +- src/Hume/Core/MultipartFormRequest.cs | 6 +- src/Hume/Core/Page.cs | 33 + src/Hume/Core/Pager.cs | 54 +- src/Hume/Core/Public/ClientOptions.cs | 2 +- src/Hume/Core/Public/RequestOptions.cs | 2 +- src/Hume/Core/Public/Version.cs | 2 +- src/Hume/Core/QueryStringBuilder.cs | 244 +++- src/Hume/Core/RawClient.cs | 1 + src/Hume/Core/StringEnum.cs | 2 - src/Hume/Core/StringEnumSerializer.cs | 25 - src/Hume/Core/ValueConvert.cs | 3 +- src/Hume/Core/WebSockets/Event.cs | 34 +- src/Hume/Core/WebSockets/ReconnectStrategy.cs | 90 ++ src/Hume/Core/WebSockets/ReconnectionInfo.cs | 2 +- src/Hume/Core/WebSockets/ReconnectionType.cs | 2 +- src/Hume/Core/WebSockets/WebSocketClient.cs | 162 ++- .../WebSockets/WebSocketConnection.Sending.cs | 330 ++++- .../Core/WebSockets/WebSocketConnection.cs | 587 +++++++- src/Hume/Core/WithRawResponse.cs | 18 - src/Hume/Core/WithRawResponseTask.cs | 144 -- src/Hume/EmpathicVoice/Chat/ChatApi.cs | 112 +- src/Hume/EmpathicVoice/Chat/IChatApi.cs | 30 + .../ChatGroups/ChatGroupsClient.cs | 55 +- .../ChatGroups/IChatGroupsClient.cs | 4 +- .../Requests/ChatGroupsGetAudioRequest.cs | 2 +- .../Requests/ChatGroupsGetChatGroupRequest.cs | 2 +- .../ChatGroupsListChatGroupEventsRequest.cs | 2 +- .../ChatGroupsListChatGroupsRequest.cs | 2 +- src/Hume/EmpathicVoice/Chats/ChatsClient.cs | 43 +- src/Hume/EmpathicVoice/Chats/IChatsClient.cs | 4 +- .../Requests/ChatsListChatEventsRequest.cs | 2 +- .../Chats/Requests/ChatsListChatsRequest.cs | 2 +- .../EmpathicVoice/Configs/ConfigsClient.cs | 99 +- .../EmpathicVoice/Configs/IConfigsClient.cs | 8 +- .../ConfigsListConfigVersionsRequest.cs | 2 +- .../Requests/ConfigsListConfigsRequest.cs | 2 +- .../Configs/Requests/PostedConfig.cs | 8 +- .../Configs/Requests/PostedConfigName.cs | 2 +- .../Configs/Requests/PostedConfigVersion.cs | 8 +- .../PostedConfigVersionDescription.cs | 2 +- .../ControlPlane/ControlPlaneApi.cs | 118 +- .../ControlPlane/ControlPlaneClient.cs | 15 +- .../ControlPlane/IControlPlaneApi.cs | 29 + .../ControlPlane/IControlPlaneClient.cs | 2 +- src/Hume/EmpathicVoice/EmpathicVoiceClient.cs | 10 +- .../EmpathicVoice/IEmpathicVoiceClient.cs | 5 +- .../EmpathicVoice/Prompts/IPromptsClient.cs | 6 +- .../EmpathicVoice/Prompts/PromptsClient.cs | 95 +- .../Prompts/Requests/PostedPrompt.cs | 2 +- .../Prompts/Requests/PostedPromptName.cs | 2 +- .../Prompts/Requests/PostedPromptVersion.cs | 2 +- .../PostedPromptVersionDescription.cs | 2 +- .../PromptsListPromptVersionsRequest.cs | 2 +- .../Requests/PromptsListPromptsRequest.cs | 2 +- src/Hume/EmpathicVoice/Tools/IToolsClient.cs | 8 +- .../Tools/Requests/PostedUserDefinedTool.cs | 2 +- .../Requests/PostedUserDefinedToolName.cs | 2 +- .../Requests/PostedUserDefinedToolVersion.cs | 2 +- ...PostedUserDefinedToolVersionDescription.cs | 2 +- .../Requests/ToolsListToolVersionsRequest.cs | 2 +- .../Tools/Requests/ToolsListToolsRequest.cs | 2 +- src/Hume/EmpathicVoice/Tools/ToolsClient.cs | 99 +- src/Hume/EmpathicVoice/Types/AssistantEnd.cs | 95 +- .../EmpathicVoice/Types/AssistantInput.cs | 95 +- .../EmpathicVoice/Types/AssistantMessage.cs | 95 +- .../EmpathicVoice/Types/AssistantProsody.cs | 95 +- .../EmpathicVoice/Types/AudioConfiguration.cs | 18 +- src/Hume/EmpathicVoice/Types/AudioInput.cs | 95 +- src/Hume/EmpathicVoice/Types/AudioOutput.cs | 95 +- src/Hume/EmpathicVoice/Types/BuiltInTool.cs | 54 +- .../EmpathicVoice/Types/BuiltinToolConfig.cs | 4 +- src/Hume/EmpathicVoice/Types/ChatMessage.cs | 4 +- src/Hume/EmpathicVoice/Types/ChatMetadata.cs | 95 +- .../Types/ConnectSessionSettings.cs | 4 +- .../Types/ConnectSessionSettingsAudio.cs | 4 +- .../Types/ConnectSessionSettingsContext.cs | 4 +- src/Hume/EmpathicVoice/Types/Context.cs | 4 +- src/Hume/EmpathicVoice/Types/ContextType.cs | 54 +- src/Hume/EmpathicVoice/Types/EmotionScores.cs | 4 +- src/Hume/EmpathicVoice/Types/Encoding.cs | 77 ++ src/Hume/EmpathicVoice/Types/ErrorLevel.cs | 77 ++ src/Hume/EmpathicVoice/Types/ErrorResponse.cs | 4 +- .../Types/HttpValidationError.cs | 4 +- src/Hume/EmpathicVoice/Types/Inference.cs | 4 +- .../EmpathicVoice/Types/InterruptionSpec.cs | 29 + .../EmpathicVoice/Types/LanguageModelType.cs | 54 +- .../Types/MillisecondInterval.cs | 4 +- .../EmpathicVoice/Types/ModelProviderEnum.cs | 54 +- .../Types/PauseAssistantMessage.cs | 95 +- .../EmpathicVoice/Types/PostedBuiltinTool.cs | 4 +- .../Types/PostedBuiltinToolName.cs | 54 +- .../Types/PostedConfigPromptSpec.cs | 4 +- .../EmpathicVoice/Types/PostedEllmModel.cs | 4 +- .../Types/PostedEventMessageSpec.cs | 4 +- .../Types/PostedEventMessageSpecs.cs | 4 +- .../Types/PostedInterruptionSpec.cs | 35 + .../Types/PostedLanguageModel.cs | 4 +- .../EmpathicVoice/Types/PostedNudgeSpec.cs | 4 +- .../EmpathicVoice/Types/PostedTimeoutSpec.cs | 4 +- .../EmpathicVoice/Types/PostedTimeoutSpecs.cs | 4 +- .../Types/PostedTimeoutSpecsInactivity.cs | 4 +- .../Types/PostedTimeoutSpecsMaxDuration.cs | 4 +- .../Types/PostedTurnDetectionSpec.cs | 47 + .../Types/PostedUserDefinedToolSpec.cs | 4 +- .../Types/PostedWebhookEventType.cs | 54 +- .../EmpathicVoice/Types/PostedWebhookSpec.cs | 4 +- .../Types/PromptExpansionSpec.cs | 4 +- .../EmpathicVoice/Types/ProsodyInference.cs | 4 +- .../Types/ResumeAssistantMessage.cs | 95 +- .../EmpathicVoice/Types/ReturnBuiltinTool.cs | 4 +- .../Types/ReturnBuiltinToolToolType.cs | 54 +- src/Hume/EmpathicVoice/Types/ReturnChat.cs | 4 +- .../Types/ReturnChatAudioReconstruction.cs | 4 +- .../ReturnChatAudioReconstructionStatus.cs | 57 +- .../EmpathicVoice/Types/ReturnChatEvent.cs | 4 +- .../Types/ReturnChatEventRole.cs | 54 +- .../Types/ReturnChatEventType.cs | 54 +- .../EmpathicVoice/Types/ReturnChatGroup.cs | 4 +- ...eturnChatGroupPagedAudioReconstructions.cs | 4 +- ...AudioReconstructionsPaginationDirection.cs | 55 +- .../Types/ReturnChatGroupPagedChats.cs | 4 +- ...nChatGroupPagedChatsPaginationDirection.cs | 57 +- .../Types/ReturnChatGroupPagedEvents.cs | 4 +- ...ChatGroupPagedEventsPaginationDirection.cs | 57 +- .../Types/ReturnChatPagedEvents.cs | 4 +- ...eturnChatPagedEventsPaginationDirection.cs | 57 +- .../Types/ReturnChatPagedEventsStatus.cs | 55 +- .../EmpathicVoice/Types/ReturnChatStatus.cs | 54 +- src/Hume/EmpathicVoice/Types/ReturnConfig.cs | 10 +- .../EmpathicVoice/Types/ReturnConfigSpec.cs | 4 +- .../EmpathicVoice/Types/ReturnEllmModel.cs | 4 +- .../Types/ReturnEventMessageSpec.cs | 4 +- .../Types/ReturnEventMessageSpecs.cs | 4 +- .../Types/ReturnInterruptionSpec.cs | 35 + .../Types/ReturnLanguageModel.cs | 4 +- .../EmpathicVoice/Types/ReturnNudgeSpec.cs | 4 +- .../Types/ReturnPagedChatGroups.cs | 4 +- ...eturnPagedChatGroupsPaginationDirection.cs | 57 +- .../EmpathicVoice/Types/ReturnPagedChats.cs | 4 +- .../ReturnPagedChatsPaginationDirection.cs | 57 +- .../EmpathicVoice/Types/ReturnPagedConfigs.cs | 4 +- .../EmpathicVoice/Types/ReturnPagedPrompts.cs | 4 +- .../Types/ReturnPagedUserDefinedTools.cs | 4 +- src/Hume/EmpathicVoice/Types/ReturnPrompt.cs | 4 +- .../Types/ReturnPromptVersionType.cs | 54 +- .../EmpathicVoice/Types/ReturnTimeoutSpec.cs | 4 +- .../EmpathicVoice/Types/ReturnTimeoutSpecs.cs | 4 +- .../Types/ReturnTurnDetectionSpec.cs | 47 + .../Types/ReturnUserDefinedTool.cs | 4 +- .../Types/ReturnUserDefinedToolToolType.cs | 55 +- .../Types/ReturnUserDefinedToolVersionType.cs | 55 +- src/Hume/EmpathicVoice/Types/ReturnVoice.cs | 4 +- .../Types/ReturnWebhookEventType.cs | 54 +- .../EmpathicVoice/Types/ReturnWebhookSpec.cs | 4 +- src/Hume/EmpathicVoice/Types/Role.cs | 50 +- .../EmpathicVoice/Types/SessionSettings.cs | 95 +- src/Hume/EmpathicVoice/Types/Tool.cs | 4 +- .../EmpathicVoice/Types/ToolCallMessage.cs | 91 +- .../EmpathicVoice/Types/ToolErrorMessage.cs | 92 +- .../Types/ToolResponseMessage.cs | 95 +- src/Hume/EmpathicVoice/Types/ToolType.cs | 54 +- .../EmpathicVoice/Types/TurnDetectionSpec.cs | 35 + src/Hume/EmpathicVoice/Types/UserInput.cs | 92 +- .../EmpathicVoice/Types/UserInterruption.cs | 95 +- src/Hume/EmpathicVoice/Types/UserMessage.cs | 95 +- .../EmpathicVoice/Types/ValidationError.cs | 4 +- src/Hume/EmpathicVoice/Types/VoiceId.cs | 4 +- src/Hume/EmpathicVoice/Types/VoiceName.cs | 4 +- src/Hume/EmpathicVoice/Types/VoiceProvider.cs | 54 +- .../EmpathicVoice/Types/WebSocketError.cs | 91 +- .../EmpathicVoice/Types/WebhookEventBase.cs | 4 +- .../Types/WebhookEventChatEnded.cs | 4 +- .../Types/WebhookEventChatStartType.cs | 54 +- .../Types/WebhookEventChatStarted.cs | 4 +- .../Types/WebhookEventChatStatus.cs | 54 +- .../Types/WebhookEventToolCall.cs | 4 +- .../Batch/BatchClient.cs | 74 +- .../Batch/IBatchClient.cs | 2 +- .../Batch/Requests/BatchListJobsRequest.cs | 2 +- ...chStartInferenceJobFromLocalFileRequest.cs | 2 +- .../Batch/Types/Alternative.cs | 77 ++ .../Batch/Types/Bcp47Tag.cs | 54 +- .../Batch/Types/BoundingBox.cs | 4 +- .../Batch/Types/BurstPrediction.cs | 4 +- .../Types/CompletedEmbeddingGeneration.cs | 4 +- .../Batch/Types/CompletedInference.cs | 4 +- .../Batch/Types/CompletedState.cs | 4 +- .../Batch/Types/CompletedTlInference.cs | 4 +- .../Batch/Types/CompletedTraining.cs | 4 +- .../Batch/Types/CustomModelId.cs | 4 +- .../Batch/Types/CustomModelPrediction.cs | 4 +- .../Batch/Types/CustomModelRequest.cs | 4 +- .../Batch/Types/CustomModelVersionId.cs | 4 +- .../Batch/Types/CustomModelsInferenceJob.cs | 4 +- .../Batch/Types/CustomModelsTrainingJob.cs | 4 +- .../Batch/Types/DatasetId.cs | 4 +- .../Batch/Types/DatasetVersionId.cs | 4 +- .../Batch/Types/DescriptionsScore.cs | 4 +- .../Batch/Types/Direction.cs | 54 +- .../Types/EmbeddingGenerationBaseRequest.cs | 4 +- .../Batch/Types/EmbeddingGenerationJob.cs | 4 +- .../Batch/Types/EmotionScore.cs | 4 +- .../Batch/Types/Error.cs | 4 +- .../Batch/Types/EvaluationArgs.cs | 4 +- .../ExpressionMeasurement/Batch/Types/Face.cs | 4 +- .../Batch/Types/FacePrediction.cs | 4 +- .../Batch/Types/FacemeshPrediction.cs | 4 +- .../Batch/Types/FacsScore.cs | 4 +- .../Batch/Types/Failed.cs | 4 +- .../Batch/Types/FailedState.cs | 4 +- .../ExpressionMeasurement/Batch/Types/File.cs | 4 +- .../Batch/Types/Granularity.cs | 54 +- .../GroupedPredictionsBurstPrediction.cs | 4 +- .../Types/GroupedPredictionsFacePrediction.cs | 4 +- .../GroupedPredictionsFacemeshPrediction.cs | 4 +- .../GroupedPredictionsLanguagePrediction.cs | 4 +- .../Types/GroupedPredictionsNerPrediction.cs | 4 +- .../GroupedPredictionsProsodyPrediction.cs | 4 +- .../Batch/Types/InProgress.cs | 4 +- .../Batch/Types/InProgressState.cs | 4 +- .../Batch/Types/InferenceBaseRequest.cs | 4 +- .../Batch/Types/InferenceJob.cs | 4 +- .../Batch/Types/InferencePrediction.cs | 4 +- .../Batch/Types/InferenceRequest.cs | 4 +- .../Batch/Types/InferenceResults.cs | 4 +- .../Types/InferenceSourcePredictResult.cs | 4 +- .../Batch/Types/JobEmbeddingGeneration.cs | 4 +- .../Batch/Types/JobId.cs | 4 +- .../Batch/Types/JobInference.cs | 4 +- .../Batch/Types/JobTlInference.cs | 4 +- .../Batch/Types/JobTraining.cs | 4 +- .../Batch/Types/Language.cs | 4 +- .../Batch/Types/LanguagePrediction.cs | 4 +- .../Batch/Types/Models.cs | 4 +- .../Batch/Types/ModelsPredictions.cs | 4 +- .../ExpressionMeasurement/Batch/Types/Ner.cs | 4 +- .../Batch/Types/NerPrediction.cs | 4 +- .../Batch/Types/PositionInterval.cs | 4 +- .../PredictionsOptionalNullBurstPrediction.cs | 4 +- .../PredictionsOptionalNullFacePrediction.cs | 4 +- ...edictionsOptionalNullFacemeshPrediction.cs | 4 +- ...TranscriptionMetadataLanguagePrediction.cs | 4 +- ...ionalTranscriptionMetadataNerPrediction.cs | 4 +- ...lTranscriptionMetadataProsodyPrediction.cs | 4 +- .../Batch/Types/Prosody.cs | 4 +- .../Batch/Types/ProsodyPrediction.cs | 4 +- .../Batch/Types/Queued.cs | 4 +- .../Batch/Types/QueuedState.cs | 4 +- .../Batch/Types/RegistryFileDetail.cs | 4 +- .../Batch/Types/SentimentScore.cs | 4 +- .../Batch/Types/SortBy.cs | 54 +- .../Batch/Types/Source.cs | 37 +- .../Batch/Types/SourceFile.cs | 4 +- .../Batch/Types/SourceTextSource.cs | 4 +- .../Batch/Types/SourceUrl.cs | 4 +- .../Batch/Types/StateEmbeddingGeneration.cs | 43 +- ...gGenerationCompletedEmbeddingGeneration.cs | 4 +- .../Types/StateEmbeddingGenerationFailed.cs | 4 +- .../StateEmbeddingGenerationInProgress.cs | 4 +- .../Types/StateEmbeddingGenerationQueued.cs | 4 +- .../Batch/Types/StateInference.cs | 39 +- .../Batch/Types/StateTlInference.cs | 39 +- .../StateTlInferenceCompletedTlInference.cs | 4 +- .../Batch/Types/StateTlInferenceFailed.cs | 4 +- .../Batch/Types/StateTlInferenceInProgress.cs | 4 +- .../Batch/Types/StateTlInferenceQueued.cs | 4 +- .../Batch/Types/StateTraining.cs | 39 +- .../Types/StateTrainingCompletedTraining.cs | 4 +- .../Batch/Types/StateTrainingFailed.cs | 4 +- .../Batch/Types/StateTrainingInProgress.cs | 4 +- .../Batch/Types/StateTrainingQueued.cs | 4 +- .../Batch/Types/Status.cs | 54 +- .../ExpressionMeasurement/Batch/Types/Tag.cs | 4 +- .../ExpressionMeasurement/Batch/Types/Task.cs | 35 +- .../Batch/Types/TaskClassification.cs | 4 +- .../Batch/Types/TaskRegression.cs | 4 +- .../Batch/Types/TimeInterval.cs | 4 +- .../Batch/Types/TlInferenceBaseRequest.cs | 4 +- .../Batch/Types/TlInferencePrediction.cs | 4 +- .../Batch/Types/TlInferenceResults.cs | 4 +- .../Types/TlInferenceSourcePredictResult.cs | 4 +- .../Batch/Types/ToxicityScore.cs | 4 +- .../Batch/Types/TrainingBaseRequest.cs | 4 +- .../Batch/Types/TrainingCustomModel.cs | 4 +- .../Batch/Types/Transcription.cs | 4 +- .../Batch/Types/TranscriptionMetadata.cs | 4 +- .../ExpressionMeasurement/Batch/Types/Type.cs | 50 +- .../ExpressionMeasurement/Batch/Types/Url.cs | 4 +- .../Batch/Types/ValidationArgs.cs | 4 +- .../ExpressionMeasurement/Batch/Types/When.cs | 50 +- .../Batch/Types/Window.cs | 4 +- .../ExpressionMeasurementClient.cs | 2 +- .../Stream/IStreamClient.cs | 7 +- .../Stream/Stream/IStreamApi.cs | 20 + .../Stream/Stream/StreamApi.cs | 112 +- .../Stream/Stream/Types/Config.cs | 4 +- .../Stream/Stream/Types/JobDetails.cs | 4 +- .../Stream/Stream/Types/StreamErrorMessage.cs | 4 +- .../Stream/Stream/Types/StreamFace.cs | 4 +- .../Stream/Stream/Types/StreamLanguage.cs | 4 +- .../Stream/Types/StreamModelPredictions.cs | 4 +- .../Types/StreamModelPredictionsBurst.cs | 4 +- ...eamModelPredictionsBurstPredictionsItem.cs | 4 +- .../Types/StreamModelPredictionsFace.cs | 4 +- ...reamModelPredictionsFacePredictionsItem.cs | 4 +- .../Types/StreamModelPredictionsFacemesh.cs | 4 +- ...ModelPredictionsFacemeshPredictionsItem.cs | 4 +- .../Types/StreamModelPredictionsJobDetails.cs | 4 +- .../Types/StreamModelPredictionsLanguage.cs | 4 +- ...ModelPredictionsLanguagePredictionsItem.cs | 4 +- .../Types/StreamModelPredictionsProsody.cs | 4 +- ...mModelPredictionsProsodyPredictionsItem.cs | 4 +- .../Types/StreamModelsEndpointPayload.cs | 4 +- .../Stream/Types/StreamWarningMessage.cs | 4 +- .../Types/StreamWarningMessageJobDetails.cs | 4 +- .../Stream/StreamClient.cs | 10 +- .../Stream/Types/EmotionEmbeddingItem.cs | 4 +- .../Stream/Types/SentimentItem.cs | 4 +- .../Stream/Types/StreamBoundingBox.cs | 4 +- .../Stream/Types/TextPosition.cs | 4 +- .../Stream/Types/TimeRange.cs | 4 +- .../Stream/Types/ToxicityItem.cs | 4 +- src/Hume/Hume.csproj | 14 +- src/Hume/HumeClient.cs | 2 +- src/Hume/Tts/ITtsClient.cs | 10 +- src/Hume/Tts/StreamInput/IStreamInputApi.cs | 20 + src/Hume/Tts/StreamInput/StreamInputApi.cs | 116 +- src/Hume/Tts/TtsClient.cs | 78 +- src/Hume/Tts/Types/AudioEncoding.cs | 4 +- src/Hume/Tts/Types/AudioFormatType.cs | 54 +- src/Hume/Tts/Types/ErrorResponse.cs | 4 +- src/Hume/Tts/Types/FormatMp3.cs | 91 +- src/Hume/Tts/Types/FormatPcm.cs | 91 +- src/Hume/Tts/Types/FormatWav.cs | 91 +- src/Hume/Tts/Types/HttpValidationError.cs | 4 +- src/Hume/Tts/Types/MillisecondInterval.cs | 4 +- src/Hume/Tts/Types/OctaveVersion.cs | 54 +- .../Types/PostedContextWithGenerationId.cs | 4 +- .../Tts/Types/PostedContextWithUtterances.cs | 4 +- src/Hume/Tts/Types/PostedTts.cs | 4 +- src/Hume/Tts/Types/PostedUtterance.cs | 4 +- .../Tts/Types/PostedUtteranceVoiceWithId.cs | 4 +- .../Tts/Types/PostedUtteranceVoiceWithName.cs | 4 +- src/Hume/Tts/Types/PublishTts.cs | 4 +- src/Hume/Tts/Types/ReturnGeneration.cs | 4 +- src/Hume/Tts/Types/ReturnPagedVoices.cs | 4 +- src/Hume/Tts/Types/ReturnTts.cs | 4 +- src/Hume/Tts/Types/ReturnVoice.cs | 4 +- src/Hume/Tts/Types/Snippet.cs | 4 +- src/Hume/Tts/Types/SnippetAudioChunk.cs | 91 +- src/Hume/Tts/Types/Timestamp.cs | 4 +- src/Hume/Tts/Types/TimestampMessage.cs | 91 +- src/Hume/Tts/Types/TimestampType.cs | 54 +- src/Hume/Tts/Types/ValidationError.cs | 4 +- src/Hume/Tts/Types/VoiceProvider.cs | 54 +- src/Hume/Tts/Voices/IVoicesClient.cs | 4 +- src/Hume/Tts/Voices/Requests/PostedVoice.cs | 2 +- .../Voices/Requests/VoicesDeleteRequest.cs | 2 +- .../Tts/Voices/Requests/VoicesListRequest.cs | 9 +- src/Hume/Tts/Voices/VoicesClient.cs | 34 +- 445 files changed, 11970 insertions(+), 1759 deletions(-) delete mode 100644 src/Hume.Test/Core/Json/StringEnumSerializerTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/AsyncLockTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/DisconnectionInfoTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/E2eWebSocketTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/EventTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/QueryTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/ReconnectStrategyTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/ReconnectionInfoTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/RequestMessageTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/TestWebSocketServer.cs create mode 100644 src/Hume.Test/Core/WebSockets/WebSocketClientTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/WebSocketConnectionTests.cs create mode 100644 src/Hume.Test/Core/WebSockets/WebsocketExceptionTests.cs rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => ChatGroups}/GetAudioTest.cs (92%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => ChatGroups}/GetChatGroupTest.cs (92%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => ChatGroups}/ListChatGroupEventsTest.cs (99%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => ChatGroups}/ListChatGroupsTest.cs (91%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{GetAudioTest_.cs => Chats/GetAudioTest.cs} (85%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Chats}/ListChatEventsTest.cs (99%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Chats}/ListChatsTest.cs (92%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/CreateConfigTest.cs (97%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/CreateConfigVersionTest.cs (97%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/DeleteConfigTest.cs (87%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/DeleteConfigVersionTest.cs (87%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/GetConfigVersionTest.cs (95%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/ListConfigVersionsTest.cs (95%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/ListConfigsTest.cs (95%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/UpdateConfigDescriptionTest.cs (95%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Configs}/UpdateConfigNameTest.cs (91%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => ControlPlane}/SendTest.cs (78%) delete mode 100644 src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteToolVersionTest.cs rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/CreatePromptTest.cs (93%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/CreatePromptVersionTest.cs (94%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/DeletePromptTest.cs (87%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/DeletePromptVersionTest.cs (87%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/GetPromptVersionTest.cs (90%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/ListPromptVersionsTest.cs (91%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/ListPromptsTest.cs (94%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/UpdatePromptDescriptionTest.cs (92%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Prompts}/UpdatePromptNameTest.cs (91%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Tools}/CreateToolTest.cs (95%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Tools}/CreateToolVersionTest.cs (95%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Tools}/DeleteToolTest.cs (87%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Tools}/GetToolVersionTest.cs (91%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Tools}/ListToolVersionsTest.cs (93%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Tools}/ListToolsTest.cs (94%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Tools}/UpdateToolDescriptionTest.cs (94%) rename src/Hume.Test/Unit/MockServer/EmpathicVoice/{ => Tools}/UpdateToolNameTest.cs (91%) rename src/Hume.Test/Unit/MockServer/Tts/{ => Voices}/CreateTest.cs (89%) rename src/Hume.Test/Unit/MockServer/Tts/{ => Voices}/DeleteTest.cs (89%) rename src/Hume.Test/Unit/MockServer/Tts/{ => Voices}/ListTest.cs (90%) create mode 100644 src/Hume/Core/HttpContentExtensions.cs delete mode 100644 src/Hume/Core/StringEnumSerializer.cs create mode 100644 src/Hume/Core/WebSockets/ReconnectStrategy.cs delete mode 100644 src/Hume/Core/WithRawResponse.cs delete mode 100644 src/Hume/Core/WithRawResponseTask.cs create mode 100644 src/Hume/EmpathicVoice/Chat/IChatApi.cs create mode 100644 src/Hume/EmpathicVoice/ControlPlane/IControlPlaneApi.cs create mode 100644 src/Hume/EmpathicVoice/Types/Encoding.cs create mode 100644 src/Hume/EmpathicVoice/Types/ErrorLevel.cs create mode 100644 src/Hume/EmpathicVoice/Types/InterruptionSpec.cs create mode 100644 src/Hume/EmpathicVoice/Types/PostedInterruptionSpec.cs create mode 100644 src/Hume/EmpathicVoice/Types/PostedTurnDetectionSpec.cs create mode 100644 src/Hume/EmpathicVoice/Types/ReturnInterruptionSpec.cs create mode 100644 src/Hume/EmpathicVoice/Types/ReturnTurnDetectionSpec.cs create mode 100644 src/Hume/EmpathicVoice/Types/TurnDetectionSpec.cs create mode 100644 src/Hume/ExpressionMeasurement/Batch/Types/Alternative.cs create mode 100644 src/Hume/ExpressionMeasurement/Stream/Stream/IStreamApi.cs create mode 100644 src/Hume/Tts/StreamInput/IStreamInputApi.cs diff --git a/.fern/metadata.json b/.fern/metadata.json index fb31251..868f30f 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,7 +1,7 @@ { - "cliVersion": "3.91.2", + "cliVersion": "4.65.2", "generatorName": "fernapi/fern-csharp-sdk", - "generatorVersion": "2.22.7", + "generatorVersion": "2.59.5", "generatorConfig": { "namespace": "Hume", "client-class-name": "HumeClient", @@ -25,5 +25,6 @@ } } }, - "sdkVersion": "0.2.6" + "originGitCommit": "089414edddad02c1799299617e095fcd687046cf", + "sdkVersion": "0.2.7" } \ No newline at end of file diff --git a/README.md b/README.md index e2a6d7d..149d75a 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,15 @@ The Hume C# library provides convenient access to the Hume APIs from C#. - [Installation](#installation) - [Reference](#reference) - [Usage](#usage) +- [Environments](#environments) - [Exception Handling](#exception-handling) - [Pagination](#pagination) - [Advanced](#advanced) - [Retries](#retries) - [Timeouts](#timeouts) - [Raw Response](#raw-response) + - [Additional Headers](#additional-headers) + - [Additional Query Parameters](#additional-query-parameters) - [Forward Compatible Enums](#forward-compatible-enums) - [Contributing](#contributing) @@ -43,10 +46,20 @@ using Hume.EmpathicVoice; using Hume; var client = new HumeClient("API_KEY"); -await client.EmpathicVoice.ControlPlane.SendAsync( - "chat_id", - new SessionSettings { Type = "session_settings" } -); +await client.EmpathicVoice.ControlPlane.SendAsync("chat_id", new SessionSettings()); +``` + +## Environments + +This SDK allows you to configure different environments for API requests. + +```csharp +using Hume; + +var client = new HumeClient(new ClientOptions +{ + Environment = HumeClientEnvironment.Prod +}); ``` ## Exception Handling @@ -156,6 +169,38 @@ if (headers.TryGetValue("X-Request-Id", out var requestId)) var data = await client.EmpathicVoice.ControlPlane.SendAsync(...); ``` +### Additional Headers + +If you would like to send additional headers as part of the request, use the `AdditionalHeaders` request option. + +```csharp +var response = await client.EmpathicVoice.ControlPlane.SendAsync( + ..., + new RequestOptions { + AdditionalHeaders = new Dictionary + { + { "X-Custom-Header", "custom-value" } + } + } +); +``` + +### Additional Query Parameters + +If you would like to send additional query parameters as part of the request, use the `AdditionalQueryParameters` request option. + +```csharp +var response = await client.EmpathicVoice.ControlPlane.SendAsync( + ..., + new RequestOptions { + AdditionalQueryParameters = new Dictionary + { + { "custom_param", "custom-value" } + } + } +); +``` + ### Forward Compatible Enums This SDK uses forward-compatible enums that can handle unknown values gracefully. diff --git a/reference.md b/reference.md index d4b8413..2da6e9c 100644 --- a/reference.md +++ b/reference.md @@ -27,10 +27,7 @@ Send a message to a specific chat.
```csharp -await client.EmpathicVoice.ControlPlane.SendAsync( - "chat_id", - new SessionSettings { Type = "session_settings" } -); +await client.EmpathicVoice.ControlPlane.SendAsync("chat_id", new SessionSettings()); ```
@@ -2606,7 +2603,7 @@ await client.Tts.SynthesizeJsonAsync( }, }, }, - Format = new FormatMp3 { Type = "mp3" }, + Format = new FormatMp3(), NumGenerations = 1, Utterances = new List() { @@ -2679,7 +2676,7 @@ await client.Tts.SynthesizeFileAsync( new PostedTts { Context = new PostedContextWithGenerationId { GenerationId = "" }, - Format = new FormatMp3 { Type = "mp3" }, + Format = new FormatMp3(), NumGenerations = 1, Utterances = new List() { diff --git a/src/Hume.Test/Core/Json/DateOnlyJsonTests.cs b/src/Hume.Test/Core/Json/DateOnlyJsonTests.cs index 4608c92..6fb692e 100644 --- a/src/Hume.Test/Core/Json/DateOnlyJsonTests.cs +++ b/src/Hume.Test/Core/Json/DateOnlyJsonTests.cs @@ -73,4 +73,28 @@ public void DeserializeNullableDateOnly_ShouldMatchExpectedDateOnly() Assert.That(dateOnly, Is.EqualTo(expected)); } } + + [Test] + public void ShouldSerializeDictionaryWithDateOnlyKey() + { + var key = new DateOnly(2023, 10, 5); + var dict = new Dictionary { { key, "value_a" } }; + var json = JsonUtils.Serialize(dict); + Assert.That(json, Does.Contain("2023-10-05")); + Assert.That(json, Does.Contain("value_a")); + } + + [Test] + public void ShouldDeserializeDictionaryWithDateOnlyKey() + { + var json = """ + { + "2023-10-05": "value_a" + } + """; + var dict = JsonUtils.Deserialize>(json); + Assert.That(dict, Is.Not.Null); + var key = new DateOnly(2023, 10, 5); + Assert.That(dict![key], Is.EqualTo("value_a")); + } } diff --git a/src/Hume.Test/Core/Json/DateTimeJsonTests.cs b/src/Hume.Test/Core/Json/DateTimeJsonTests.cs index adfc375..ce58ee6 100644 --- a/src/Hume.Test/Core/Json/DateTimeJsonTests.cs +++ b/src/Hume.Test/Core/Json/DateTimeJsonTests.cs @@ -107,4 +107,28 @@ public void DeserializeNullableDateTime_ShouldMatchExpectedDateTime() Assert.That(dateTime, Is.EqualTo(expected)); } } + + [Test] + public void ShouldSerializeDictionaryWithDateTimeKey() + { + var key = new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc); + var dict = new Dictionary { { key, "value_a" } }; + var json = JsonUtils.Serialize(dict); + Assert.That(json, Does.Contain("2023-10-05T14:30:00.000Z")); + Assert.That(json, Does.Contain("value_a")); + } + + [Test] + public void ShouldDeserializeDictionaryWithDateTimeKey() + { + var json = """ + { + "2023-10-05T14:30:00.000Z": "value_a" + } + """; + var dict = JsonUtils.Deserialize>(json); + Assert.That(dict, Is.Not.Null); + var key = new DateTime(2023, 10, 5, 14, 30, 0, DateTimeKind.Utc); + Assert.That(dict![key], Is.EqualTo("value_a")); + } } diff --git a/src/Hume.Test/Core/Json/StringEnumSerializerTests.cs b/src/Hume.Test/Core/Json/StringEnumSerializerTests.cs deleted file mode 100644 index f02b0d3..0000000 --- a/src/Hume.Test/Core/Json/StringEnumSerializerTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; -using Hume.Core; -using NUnit.Framework; - -namespace Hume.Test.Core.Json; - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class StringEnumSerializerTests -{ - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - - private static readonly DummyEnum KnownEnumValue2 = DummyEnum.KnownValue2; - private static readonly DummyEnum UnknownEnumValue = DummyEnum.FromCustom("unknown_value"); - - private static readonly string JsonWithKnownEnum2 = $$""" - { - "enum_property": "{{KnownEnumValue2}}" - } - """; - - private static readonly string JsonWithUnknownEnum = $$""" - { - "enum_property": "{{UnknownEnumValue}}" - } - """; - - [Test] - public void ShouldParseKnownEnumValue2() - { - var obj = JsonSerializer.Deserialize(JsonWithKnownEnum2, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldParseUnknownEnum() - { - var obj = JsonSerializer.Deserialize(JsonWithUnknownEnum, JsonOptions); - Assert.That(obj, Is.Not.Null); - Assert.That(obj.EnumProperty, Is.EqualTo(UnknownEnumValue)); - } - - [Test] - public void ShouldSerializeKnownEnumValue2() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = KnownEnumValue2 }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(KnownEnumValue2)); - } - - [Test] - public void ShouldSerializeUnknownEnum() - { - var json = JsonSerializer.SerializeToElement( - new DummyObject { EnumProperty = UnknownEnumValue }, - JsonOptions - ); - TestContext.Out.WriteLine("Serialized JSON: \n" + json); - var enumString = json.GetProperty("enum_property").GetString(); - Assert.That(enumString, Is.Not.Null); - Assert.That(enumString, Is.EqualTo(UnknownEnumValue)); - } -} - -public class DummyObject -{ - [JsonPropertyName("enum_property")] - public DummyEnum EnumProperty { get; set; } -} - -[JsonConverter(typeof(StringEnumSerializer))] -public readonly record struct DummyEnum : IStringEnum -{ - public DummyEnum(string value) - { - Value = value; - } - - /// - /// The string value of the enum. - /// - public string Value { get; } - - public static readonly DummyEnum KnownValue1 = FromCustom(Values.KnownValue1); - - public static readonly DummyEnum KnownValue2 = FromCustom(Values.KnownValue2); - - /// - /// Constant strings for enum values - /// - public static class Values - { - public const string KnownValue1 = "known_value1"; - - public const string KnownValue2 = "known_value2"; - } - - /// - /// Create a string enum with the given value. - /// - public static DummyEnum FromCustom(string value) - { - return new DummyEnum(value); - } - - /// - /// Returns the string value of the enum. - /// - public override string ToString() - { - return Value; - } - - public bool Equals(string? other) - { - return Value.Equals(other); - } - - public override int GetHashCode() - { - return Value.GetHashCode(); - } - - public static explicit operator string(DummyEnum value) => value.Value; - - public static explicit operator DummyEnum(string value) => new(value); - - public static bool operator ==(DummyEnum value1, string value2) => value1.Value.Equals(value2); - - public static bool operator !=(DummyEnum value1, string value2) => !value1.Value.Equals(value2); -} diff --git a/src/Hume.Test/Core/Pagination/GuidCursorTest.cs b/src/Hume.Test/Core/Pagination/GuidCursorTest.cs index d95e2a7..eff4847 100644 --- a/src/Hume.Test/Core/Pagination/GuidCursorTest.cs +++ b/src/Hume.Test/Core/Pagination/GuidCursorTest.cs @@ -1,3 +1,4 @@ +using global::System.Net; using Hume.Core; using NUnit.Framework; using SystemTask = global::System.Threading.Tasks.Task; @@ -45,7 +46,7 @@ public async SystemTask CursorPagerShouldWorkWithGuidCursors() (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, (request, cursor) => { @@ -100,4 +101,16 @@ private class Cursor { public required Guid? Next { get; set; } } + + private static WithRawResponse Wrap(Response response) => + new() + { + Data = response, + RawResponse = new RawResponse + { + StatusCode = HttpStatusCode.OK, + Url = new Uri("https://localhost"), + Headers = default, + }, + }; } diff --git a/src/Hume.Test/Core/Pagination/HasNextPageOffsetTest.cs b/src/Hume.Test/Core/Pagination/HasNextPageOffsetTest.cs index dad2926..7298b55 100644 --- a/src/Hume.Test/Core/Pagination/HasNextPageOffsetTest.cs +++ b/src/Hume.Test/Core/Pagination/HasNextPageOffsetTest.cs @@ -1,3 +1,4 @@ +using global::System.Net; using Hume.Core; using NUnit.Framework; using SystemTask = global::System.Threading.Tasks.Task; @@ -41,7 +42,7 @@ public async SystemTask OffsetPagerShouldWorkWithHasNextPage() (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, request => request?.Pagination?.Page ?? 0, (request, offset) => @@ -89,4 +90,16 @@ private class Data { public IEnumerable? Items { get; set; } } + + private static WithRawResponse Wrap(Response response) => + new() + { + Data = response, + RawResponse = new RawResponse + { + StatusCode = HttpStatusCode.OK, + Url = new Uri("https://localhost"), + Headers = default, + }, + }; } diff --git a/src/Hume.Test/Core/Pagination/IntOffsetTest.cs b/src/Hume.Test/Core/Pagination/IntOffsetTest.cs index c1796ce..33c399b 100644 --- a/src/Hume.Test/Core/Pagination/IntOffsetTest.cs +++ b/src/Hume.Test/Core/Pagination/IntOffsetTest.cs @@ -1,3 +1,4 @@ +using global::System.Net; using Hume.Core; using NUnit.Framework; using SystemTask = global::System.Threading.Tasks.Task; @@ -29,7 +30,7 @@ public async SystemTask OffsetPagerShouldWorkWithIntPage() (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, request => request?.Pagination?.Page ?? 0, (request, offset) => @@ -79,4 +80,16 @@ private class Data { public IEnumerable? Items { get; set; } } + + private static WithRawResponse Wrap(Response response) => + new() + { + Data = response, + RawResponse = new RawResponse + { + StatusCode = HttpStatusCode.OK, + Url = new Uri("https://localhost"), + Headers = default, + }, + }; } diff --git a/src/Hume.Test/Core/Pagination/LongOffsetTest.cs b/src/Hume.Test/Core/Pagination/LongOffsetTest.cs index 79443b7..3982c61 100644 --- a/src/Hume.Test/Core/Pagination/LongOffsetTest.cs +++ b/src/Hume.Test/Core/Pagination/LongOffsetTest.cs @@ -1,3 +1,4 @@ +using global::System.Net; using Hume.Core; using NUnit.Framework; using SystemTask = global::System.Threading.Tasks.Task; @@ -29,7 +30,7 @@ public async SystemTask OffsetPagerShouldWorkWithLongPage() (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, request => request?.Pagination?.Page ?? 0, (request, offset) => @@ -76,4 +77,16 @@ private class Data { public IEnumerable? Items { get; set; } } + + private static WithRawResponse Wrap(Response response) => + new() + { + Data = response, + RawResponse = new RawResponse + { + StatusCode = HttpStatusCode.OK, + Url = new Uri("https://localhost"), + Headers = default, + }, + }; } diff --git a/src/Hume.Test/Core/Pagination/NoRequestCursorTest.cs b/src/Hume.Test/Core/Pagination/NoRequestCursorTest.cs index 4a7a04e..c18dfe5 100644 --- a/src/Hume.Test/Core/Pagination/NoRequestCursorTest.cs +++ b/src/Hume.Test/Core/Pagination/NoRequestCursorTest.cs @@ -1,3 +1,4 @@ +using global::System.Net; using Hume.Core; using NUnit.Framework; using SystemTask = global::System.Threading.Tasks.Task; @@ -45,7 +46,7 @@ public async SystemTask CursorPagerShouldWorkWithStringCursor() (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, (request, cursor) => { @@ -103,4 +104,16 @@ private class Cursor { public required string? Next { get; set; } } + + private static WithRawResponse Wrap(Response response) => + new() + { + Data = response, + RawResponse = new RawResponse + { + StatusCode = HttpStatusCode.OK, + Url = new Uri("https://localhost"), + Headers = default, + }, + }; } diff --git a/src/Hume.Test/Core/Pagination/NoRequestOffsetTest.cs b/src/Hume.Test/Core/Pagination/NoRequestOffsetTest.cs index 5fe3488..ba8342b 100644 --- a/src/Hume.Test/Core/Pagination/NoRequestOffsetTest.cs +++ b/src/Hume.Test/Core/Pagination/NoRequestOffsetTest.cs @@ -1,3 +1,4 @@ +using global::System.Net; using Hume.Core; using NUnit.Framework; using SystemTask = global::System.Threading.Tasks.Task; @@ -29,7 +30,7 @@ public async SystemTask OffsetPagerShouldWorkWithoutRequest() (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, request => request?.Pagination?.Page ?? 0, (request, offset) => @@ -79,4 +80,16 @@ private class Data { public IEnumerable? Items { get; set; } } + + private static WithRawResponse Wrap(Response response) => + new() + { + Data = response, + RawResponse = new RawResponse + { + StatusCode = HttpStatusCode.OK, + Url = new Uri("https://localhost"), + Headers = default, + }, + }; } diff --git a/src/Hume.Test/Core/Pagination/StepOffsetTest.cs b/src/Hume.Test/Core/Pagination/StepOffsetTest.cs index 354590b..5bcdd32 100644 --- a/src/Hume.Test/Core/Pagination/StepOffsetTest.cs +++ b/src/Hume.Test/Core/Pagination/StepOffsetTest.cs @@ -1,3 +1,4 @@ +using global::System.Net; using Hume.Core; using NUnit.Framework; using SystemTask = global::System.Threading.Tasks.Task; @@ -30,7 +31,7 @@ public async SystemTask OffsetPagerShouldWorkWithStep() (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, request => request?.Pagination?.ItemOffset ?? 0, (request, offset) => @@ -89,4 +90,16 @@ private class Data { public IEnumerable? Items { get; set; } } + + private static WithRawResponse Wrap(Response response) => + new() + { + Data = response, + RawResponse = new RawResponse + { + StatusCode = HttpStatusCode.OK, + Url = new Uri("https://localhost"), + Headers = default, + }, + }; } diff --git a/src/Hume.Test/Core/Pagination/StringCursorTest.cs b/src/Hume.Test/Core/Pagination/StringCursorTest.cs index e01029b..33b8465 100644 --- a/src/Hume.Test/Core/Pagination/StringCursorTest.cs +++ b/src/Hume.Test/Core/Pagination/StringCursorTest.cs @@ -1,3 +1,4 @@ +using global::System.Net; using Hume.Core; using NUnit.Framework; using SystemTask = global::System.Threading.Tasks.Task; @@ -46,7 +47,7 @@ public async SystemTask CursorPagerShouldWorkWithStringCursor() (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, (request, cursor) => { @@ -115,7 +116,7 @@ public async SystemTask CursorPagerShouldWorkWithStringCursor_EmptyStringCursor( (_, _, _) => { responses.MoveNext(); - return SystemTask.FromResult(responses.Current); + return SystemTask.FromResult(Wrap(responses.Current)); }, (request, cursor) => { @@ -164,4 +165,16 @@ private class Cursor { public required string? Next { get; set; } } + + private static WithRawResponse Wrap(Response response) => + new() + { + Data = response, + RawResponse = new RawResponse + { + StatusCode = HttpStatusCode.OK, + Url = new Uri("https://localhost"), + Headers = default, + }, + }; } diff --git a/src/Hume.Test/Core/QueryStringBuilderTests.cs b/src/Hume.Test/Core/QueryStringBuilderTests.cs index 5530ebd..0fb0831 100644 --- a/src/Hume.Test/Core/QueryStringBuilderTests.cs +++ b/src/Hume.Test/Core/QueryStringBuilderTests.cs @@ -46,7 +46,7 @@ public void Build_SpecialCharacters() Assert.That( result, Is.EqualTo( - "?email=test%40example.com&url=https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue&special=a%2Bb%3Dc%26d" + "?email=test@example.com&url=https://example.com/path?query=value&special=a%2Bb=c%26d" ) ); } @@ -159,7 +159,7 @@ public void Build_ReservedCharacters_NotEncoded() var result = QueryStringBuilder.Build(parameters); - // Unreserved characters: A-Z a-z 0-9 - _ . ~ + // Safe query characters include RFC 3986 unreserved + sub-delimiters (except & = +) + : @ / Assert.That(result, Is.EqualTo("?path=some-path&id=123-456_789.test~value")); } @@ -557,4 +557,102 @@ public void Builder_Set_WithCollection_ReplacesAllPreviousValues() Assert.That(result, Does.Not.EndWith("id=1")); Assert.That(result, Does.Not.EndWith("id=2")); } + + [Test] + public void EncodePathSegment_UnreservedChars_NotEncoded() + { + var result = QueryStringBuilder.EncodePathSegment("hello-world_test.value~123"); + Assert.That(result, Is.EqualTo("hello-world_test.value~123")); + } + + [Test] + public void EncodePathSegment_SubDelimiters_NotEncoded() + { + // All sub-delimiters are safe in path segments per RFC 3986 + var result = QueryStringBuilder.EncodePathSegment("a!b$c&d'e(f)g*h+i,j;k=l"); + Assert.That(result, Is.EqualTo("a!b$c&d'e(f)g*h+i,j;k=l")); + } + + [Test] + public void EncodePathSegment_ColonAndAt_NotEncoded() + { + var result = QueryStringBuilder.EncodePathSegment("user@host:8080"); + Assert.That(result, Is.EqualTo("user@host:8080")); + } + + [Test] + public void EncodePathSegment_SlashAndQuestion_Encoded() + { + // "/" and "?" are NOT part of pchar, so they must be encoded in path segments + var result = QueryStringBuilder.EncodePathSegment("path/with?query"); + Assert.That(result, Is.EqualTo("path%2Fwith%3Fquery")); + } + + [Test] + public void EncodePathSegment_Space_Encoded() + { + var result = QueryStringBuilder.EncodePathSegment("hello world"); + Assert.That(result, Is.EqualTo("hello%20world")); + } + + [Test] + public void EncodePathSegment_EmptyAndNull() + { + Assert.That(QueryStringBuilder.EncodePathSegment(""), Is.EqualTo("")); + Assert.That(QueryStringBuilder.EncodePathSegment(null!), Is.Null); + } + + [Test] + public void Build_QueryKeyVsValue_DifferentEncoding() + { + // "=" is safe in query values but NOT in query keys + var parameters = new List> + { + new("key=with=equals", "value=with=equals"), + }; + + var result = QueryStringBuilder.Build(parameters); + + // Key: "=" must be encoded + // Value: "=" is safe (part of query value safe chars) + Assert.That(result, Is.EqualTo("?key%3Dwith%3Dequals=value=with=equals")); + } + + [Test] + public void Build_QueryValue_QuestionMarkNotEncoded() + { + // "?" is safe in both query keys and query values per RFC 3986 + var parameters = new List> { new("q?key", "is this?") }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo("?q?key=is%20this?")); + } + + [Test] + public void Build_QueryKey_PlusEncoded() + { + // "+" must be encoded in both query keys and query values + var parameters = new List> { new("a+b", "c+d") }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Is.EqualTo("?a%2Bb=c%2Bd")); + } + + [Test] + public void Build_ODataFilter_DollarPreserved() + { + // "$" is safe in query keys (sub-delimiter), verifies OData-style parameters work + var parameters = new List> + { + new("$filter", "status eq 'active'"), + new("$top", "10"), + }; + + var result = QueryStringBuilder.Build(parameters); + + Assert.That(result, Does.Contain("$filter=status%20eq%20'active'")); + Assert.That(result, Does.Contain("$top=10")); + } } diff --git a/src/Hume.Test/Core/RawClientTests/QueryParameterTests.cs b/src/Hume.Test/Core/RawClientTests/QueryParameterTests.cs index 8a89abc..6e1a2ee 100644 --- a/src/Hume.Test/Core/RawClientTests/QueryParameterTests.cs +++ b/src/Hume.Test/Core/RawClientTests/QueryParameterTests.cs @@ -27,7 +27,7 @@ public void QueryParameters_SpecialCharacterEscaping() .Add("space test", "hello world") .Build(); - Assert.That(queryString, Does.Contain("email=bob%2Btest%40example.com")); + Assert.That(queryString, Does.Contain("email=bob%2Btest@example.com")); Assert.That(queryString, Does.Contain("%25Complete=100")); Assert.That(queryString, Does.Contain("space%20test=hello%20world")); } diff --git a/src/Hume.Test/Core/RawClientTests/RetriesTests.cs b/src/Hume.Test/Core/RawClientTests/RetriesTests.cs index cb45493..3a210c4 100644 --- a/src/Hume.Test/Core/RawClientTests/RetriesTests.cs +++ b/src/Hume.Test/Core/RawClientTests/RetriesTests.cs @@ -1,4 +1,5 @@ using global::System.Net.Http; +using global::System.Text.Json; using Hume.Core; using NUnit.Framework; using WireMock.Server; @@ -321,8 +322,6 @@ public async SystemTask SendRequestAsync_ShouldRespectXRateLimitResetHeader() [Test] public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() { - const string expectedBody = """{"key":"value"}"""; - _server .Given(WireMockRequest.Create().WithPath("/test").UsingPost()) .InScenario("RetryWithBody") @@ -352,9 +351,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveJsonBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the JSON body + // Verify the retried request preserved the JSON body (compare parsed to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Is.EqualTo(expectedBody)); + using var actualJson = JsonDocument.Parse(retriedEntry.RequestMessage.Body!); + Assert.That(actualJson.RootElement.GetProperty("key").GetString(), Is.EqualTo("value")); } } @@ -390,9 +390,10 @@ public async SystemTask SendRequestAsync_ShouldPreserveMultipartBody_OnRetry() Assert.That(content, Is.EqualTo("Success")); Assert.That(_server.LogEntries, Has.Count.EqualTo(2)); - // Verify the retried request preserved the multipart body + // Verify the retried request preserved the multipart body (check key/value presence to ignore formatting differences) var retriedEntry = _server.LogEntries.ElementAt(1); - Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("""{"key":"value"}""")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"key\"")); + Assert.That(retriedEntry.RequestMessage.Body, Does.Contain("\"value\"")); } } diff --git a/src/Hume.Test/Core/WebSockets/AsyncLockTests.cs b/src/Hume.Test/Core/WebSockets/AsyncLockTests.cs new file mode 100644 index 0000000..d5ad1cd --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/AsyncLockTests.cs @@ -0,0 +1,153 @@ +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class AsyncLockTests +{ + [Test] + public async Task LockAsync_ProvidesMutualExclusion() + { + var asyncLock = new AsyncLock(); + var counter = 0; + var maxConcurrent = 0; + var currentConcurrent = 0; + var tasks = new List(); + + for (int i = 0; i < 10; i++) + { + tasks.Add( + Task.Run(async () => + { + using (await asyncLock.LockAsync()) + { + var c = Interlocked.Increment(ref currentConcurrent); + if (c > maxConcurrent) + Interlocked.Exchange(ref maxConcurrent, c); + + Interlocked.Increment(ref counter); + await Task.Delay(10); // Simulate work + + Interlocked.Decrement(ref currentConcurrent); + } + }) + ); + } + + await Task.WhenAll(tasks); + + Assert.That(counter, Is.EqualTo(10)); + Assert.That(maxConcurrent, Is.EqualTo(1), "Only one task should hold the lock at a time"); + } + + [Test] + public void Lock_ProvidesMutualExclusion() + { + var asyncLock = new AsyncLock(); + var counter = 0; + var tasks = new List(); + + for (int i = 0; i < 10; i++) + { + tasks.Add( + Task.Run(() => + { + using (asyncLock.Lock()) + { + counter++; + Thread.Sleep(5); + } + }) + ); + } + + Task.WhenAll(tasks).Wait(); + + Assert.That(counter, Is.EqualTo(10)); + } + + [Test] + public async Task LockAsync_IsReentrantSafe_SequentialAcquisition() + { + var asyncLock = new AsyncLock(); + var value = 0; + + using (await asyncLock.LockAsync()) + { + value = 1; + } + + using (await asyncLock.LockAsync()) + { + value = 2; + } + + Assert.That(value, Is.EqualTo(2)); + } + + [Test] + public async Task LockAsync_ReleasesOnDispose() + { + var asyncLock = new AsyncLock(); + var enteredSecond = false; + + // Acquire and release the lock + using (await asyncLock.LockAsync()) + { + // Hold the lock briefly + } + + // Should be able to acquire again immediately + using (await asyncLock.LockAsync()) + { + enteredSecond = true; + } + + Assert.That(enteredSecond, Is.True); + } + + [Test] + public async Task LockAsync_BlocksUntilRelease() + { + var asyncLock = new AsyncLock(); + var orderLog = new List(); + + var firstAcquired = new TaskCompletionSource(); + var firstRelease = new TaskCompletionSource(); + + var task1 = Task.Run(async () => + { + using (await asyncLock.LockAsync()) + { + orderLog.Add(1); + firstAcquired.SetResult(true); + await firstRelease.Task; // Hold the lock until signaled + } + }); + + await firstAcquired.Task; + + var task2 = Task.Run(async () => + { + // This should block until task1 releases + using (await asyncLock.LockAsync()) + { + orderLog.Add(2); + } + }); + + // Give task2 a moment to start waiting + await Task.Delay(50); + + // task2 shouldn't have entered the lock yet + Assert.That(orderLog, Is.EqualTo(new[] { 1 })); + + // Release the lock in task1 + firstRelease.SetResult(true); + await task1; + await task2; + + Assert.That(orderLog, Is.EqualTo(new[] { 1, 2 })); + } +} diff --git a/src/Hume.Test/Core/WebSockets/DisconnectionInfoTests.cs b/src/Hume.Test/Core/WebSockets/DisconnectionInfoTests.cs new file mode 100644 index 0000000..f1330be --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/DisconnectionInfoTests.cs @@ -0,0 +1,93 @@ +using global::System.Net.WebSockets; +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class DisconnectionInfoTests +{ + [Test] + public void Create_WithNullClient_SetsNullProperties() + { + var info = DisconnectionInfo.Create(DisconnectionType.Error, null!, new Exception("test")); + + Assert.That(info.Type, Is.EqualTo(DisconnectionType.Error)); + Assert.That(info.CloseStatus, Is.Null); + Assert.That(info.CloseStatusDescription, Is.Null); + Assert.That(info.SubProtocol, Is.Null); + Assert.That(info.Exception, Is.Not.Null); + Assert.That(info.Exception.Message, Is.EqualTo("test")); + } + + [Test] + public void Create_WithNullException_SetsNullException() + { + var info = DisconnectionInfo.Create(DisconnectionType.ByUser, null!, null!); + + Assert.That(info.Type, Is.EqualTo(DisconnectionType.ByUser)); + Assert.That(info.Exception, Is.Null); + } + + [Test] + public void CancelReconnection_DefaultsFalse() + { + var info = DisconnectionInfo.Create(DisconnectionType.Lost, null!, null!); + + Assert.That(info.CancelReconnection, Is.False); + } + + [Test] + public void CancelReconnection_CanBeSet() + { + var info = DisconnectionInfo.Create(DisconnectionType.Lost, null!, null!); + info.CancelReconnection = true; + + Assert.That(info.CancelReconnection, Is.True); + } + + [Test] + public void CancelClosing_DefaultsFalse() + { + var info = DisconnectionInfo.Create(DisconnectionType.ByServer, null!, null!); + + Assert.That(info.CancelClosing, Is.False); + } + + [Test] + public void CancelClosing_CanBeSet() + { + var info = DisconnectionInfo.Create(DisconnectionType.ByServer, null!, null!); + info.CancelClosing = true; + + Assert.That(info.CancelClosing, Is.True); + } + + [Test] + public void Create_AllDisconnectionTypes() + { + foreach (DisconnectionType type in Enum.GetValues(typeof(DisconnectionType))) + { + var info = DisconnectionInfo.Create(type, null!, null!); + Assert.That(info.Type, Is.EqualTo(type)); + } + } + + [Test] + public void Constructor_SetsAllProperties() + { + var info = new DisconnectionInfo( + DisconnectionType.ByServer, + WebSocketCloseStatus.NormalClosure, + "Normal closure", + "graphql-ws", + new InvalidOperationException("test error") + ); + + Assert.That(info.Type, Is.EqualTo(DisconnectionType.ByServer)); + Assert.That(info.CloseStatus, Is.EqualTo(WebSocketCloseStatus.NormalClosure)); + Assert.That(info.CloseStatusDescription, Is.EqualTo("Normal closure")); + Assert.That(info.SubProtocol, Is.EqualTo("graphql-ws")); + Assert.That(info.Exception, Is.InstanceOf()); + } +} diff --git a/src/Hume.Test/Core/WebSockets/E2eWebSocketTests.cs b/src/Hume.Test/Core/WebSockets/E2eWebSocketTests.cs new file mode 100644 index 0000000..6f65e59 --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/E2eWebSocketTests.cs @@ -0,0 +1,780 @@ +using global::System.Collections.Concurrent; +using global::System.Text; +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +/// +/// End-to-end integration tests that exercise the full WebSocket lifecycle +/// against a real in-process server. These tests verify composite scenarios +/// that span connect → send → receive → reconnect → close flows. +/// +[TestFixture] +[Parallelizable(ParallelScope.Children)] +public class E2eWebSocketTests +{ + private const int TimeoutMs = 3000; + + // ─────────────────────────────────────────────────────────────── + // Echo conversation: send text, receive echo, verify round-trip + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task Echo_TextRoundTrip_ReceivesEchoFromServer() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => $"echo:{msg}"; + server.Start(); + + var received = new TaskCompletionSource(); + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + received.TrySetResult(await reader.ReadToEndAsync()); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + await client.SendInstant("hello"); + + var result = await Task.WhenAny(received.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(received.Task), "Timed out waiting for echo"); + Assert.That(received.Task.Result, Is.EqualTo("echo:hello")); + } + + [Test] + public async Task Echo_BinaryRoundTrip_ReceivesEchoFromServer() + { + await using var server = new TestWebSocketServer(); + server.OnBinaryMessage = data => data; // echo back same bytes + server.Start(); + + var received = new TaskCompletionSource(); + var client = new WebSocketConnection(server.Uri); + client.BinaryMessageReceived = async stream => + { + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + received.TrySetResult(ms.ToArray()); + }; + + await client.Start(); + await client.SendInstant(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }); + + var result = await Task.WhenAny(received.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(received.Task), "Timed out waiting for binary echo"); + Assert.That(received.Task.Result, Is.EqualTo(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF })); + + await client.Stop(global::System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "done"); + client.Dispose(); + } + + // ─────────────────────────────────────────────────────────────── + // Multi-message conversation: sequential send/receive + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task Conversation_MultipleExchanges_AllEchoedCorrectly() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => $"re:{msg}"; + server.Start(); + + var responses = new ConcurrentQueue(); + var allReceived = new TaskCompletionSource(); + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + var text = await reader.ReadToEndAsync(); + responses.Enqueue(text); + if (responses.Count >= 5) + allReceived.TrySetResult(true); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + + for (int i = 0; i < 5; i++) + await client.SendInstant($"msg-{i}"); + + var result = await Task.WhenAny(allReceived.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(allReceived.Task), "Timed out waiting for all echoes"); + Assert.That(responses.Count, Is.EqualTo(5)); + + var ordered = responses.ToArray(); + for (int i = 0; i < 5; i++) + Assert.That(ordered[i], Is.EqualTo($"re:msg-{i}")); + } + + // ─────────────────────────────────────────────────────────────── + // Rapid-fire throughput: many messages sent quickly + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task RapidFire_50Messages_AllDelivered() + { + const int messageCount = 50; + await using var server = new TestWebSocketServer(); + var serverReceived = new ConcurrentBag(); + var allReceived = new TaskCompletionSource(); + server.OnTextMessage = msg => + { + serverReceived.Add(msg); + if (serverReceived.Count >= messageCount) + allReceived.TrySetResult(true); + return null; // no echo needed + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + + for (int i = 0; i < messageCount; i++) + await client.SendInstant($"rapid-{i}"); + + var result = await Task.WhenAny(allReceived.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(allReceived.Task), "Timed out waiting for all messages"); + Assert.That(serverReceived.Count, Is.EqualTo(messageCount)); + } + + [Test] + public async Task RapidFire_QueuedSend_50Messages_AllDelivered() + { + const int messageCount = 50; + await using var server = new TestWebSocketServer(); + var serverReceived = new ConcurrentBag(); + var allReceived = new TaskCompletionSource(); + server.OnTextMessage = msg => + { + serverReceived.Add(msg); + if (serverReceived.Count >= messageCount) + allReceived.TrySetResult(true); + return null; + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + + for (int i = 0; i < messageCount; i++) + { + var queued = client.Send($"queued-{i}"); + Assert.That(queued, Is.True, $"Message {i} should have been queued"); + } + + var result = await Task.WhenAny(allReceived.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(allReceived.Task), "Timed out waiting for queued messages"); + Assert.That(serverReceived.Count, Is.EqualTo(messageCount)); + } + + // ─────────────────────────────────────────────────────────────── + // Server broadcast: server pushes to client without request + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task ServerBroadcast_TextPushed_ClientReceives() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var received = new TaskCompletionSource(); + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + received.TrySetResult(await reader.ReadToEndAsync()); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + var clientConnected = server.WaitForClientAsync(); + await client.ConnectAsync(); + await clientConnected; + + await server.BroadcastTextAsync("server-push"); + + var result = await Task.WhenAny(received.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(received.Task), "Timed out waiting for broadcast"); + Assert.That(received.Task.Result, Is.EqualTo("server-push")); + } + + [Test] + public async Task ServerBroadcast_BinaryPushed_ClientReceives() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var received = new TaskCompletionSource(); + var conn = new WebSocketConnection(server.Uri); + conn.BinaryMessageReceived = async stream => + { + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + received.TrySetResult(ms.ToArray()); + }; + + var clientConnected = server.WaitForClientAsync(); + await conn.Start(); + await clientConnected; + + await server.BroadcastBinaryAsync(new byte[] { 0xCA, 0xFE }); + + var result = await Task.WhenAny(received.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(received.Task), "Timed out waiting for binary broadcast"); + Assert.That(received.Task.Result, Is.EqualTo(new byte[] { 0xCA, 0xFE })); + + await conn.Stop(global::System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "done"); + conn.Dispose(); + } + + // ─────────────────────────────────────────────────────────────── + // Multiple subscribers: verify all handlers fire + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task MultipleSubscribers_AllReceiveMessages() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => msg; // echo + server.Start(); + + int subscriber1Count = 0; + int subscriber2Count = 0; + var bothReceived = new TaskCompletionSource(); + + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + // Read and discard the stream content + using var reader = new StreamReader(stream, Encoding.UTF8); + await reader.ReadToEndAsync(); + + Interlocked.Increment(ref subscriber1Count); + Interlocked.Increment(ref subscriber2Count); + if (subscriber1Count >= 1 && subscriber2Count >= 1) + bothReceived.TrySetResult(true); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + await client.SendInstant("multi-test"); + + var result = await Task.WhenAny(bothReceived.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(bothReceived.Task), "Timed out waiting for subscribers"); + Assert.That(subscriber1Count, Is.GreaterThanOrEqualTo(1)); + Assert.That(subscriber2Count, Is.GreaterThanOrEqualTo(1)); + } + + // ─────────────────────────────────────────────────────────────── + // Status lifecycle: full connect → send → receive → close cycle + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task FullLifecycle_ConnectSendReceiveClose() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => $"ack:{msg}"; + server.Start(); + + var statusHistory = new ConcurrentQueue(); + var received = new TaskCompletionSource(); + + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + received.TrySetResult(await reader.ReadToEndAsync()); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + client.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(client.Status)) + statusHistory.Enqueue(client.Status); + }; + + // Phase 1: Connect + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Disconnected)); + await client.ConnectAsync(); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Connected)); + + // Phase 2: Send + Receive + await client.SendInstant("lifecycle-test"); + var echoResult = await Task.WhenAny(received.Task, Task.Delay(TimeoutMs)); + Assert.That(echoResult, Is.EqualTo(received.Task), "Timed out waiting for echo"); + Assert.That(received.Task.Result, Is.EqualTo("ack:lifecycle-test")); + + // Phase 3: Close + await client.CloseAsync(); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Disconnected)); + + // Verify status transitions happened + Assert.That(statusHistory, Does.Contain(ConnectionStatus.Connecting)); + Assert.That(statusHistory, Does.Contain(ConnectionStatus.Connected)); + Assert.That(statusHistory, Does.Contain(ConnectionStatus.Disconnecting)); + Assert.That(statusHistory, Does.Contain(ConnectionStatus.Disconnected)); + } + + // ─────────────────────────────────────────────────────────────── + // Reconnection: server close triggers auto-reconnect + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task Reconnection_ServerClose_ClientReconnectsAndResumes() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => $"echo:{msg}"; + server.Start(); + + var reconnected = new TaskCompletionSource(); + var postReconnectEcho = new TaskCompletionSource(); + + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + var text = await reader.ReadToEndAsync(); + // Only capture echoes after reconnection + if (reconnected.Task.IsCompleted) + postReconnectEcho.TrySetResult(text); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + IsReconnectionEnabled = true, + StateCheckInterval = TimeSpan.FromMilliseconds(50), + ReconnectTimeout = TimeSpan.FromMilliseconds(200), + ErrorReconnectTimeout = TimeSpan.FromMilliseconds(200), + Backoff = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.Zero, + MaxReconnectInterval = TimeSpan.FromMilliseconds(50), + UseJitter = false, + }, + }; + + client.Reconnecting.Subscribe(info => reconnected.TrySetResult(info)); + + await client.ConnectAsync(); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Connected)); + + // Server closes all clients -- should trigger reconnection + await server.CloseAllClientsAsync(); + + var reconnectResult = await Task.WhenAny(reconnected.Task, Task.Delay(TimeoutMs)); + Assert.That( + reconnectResult, + Is.EqualTo(reconnected.Task), + "Timed out waiting for reconnection" + ); + + // Wait for Connected status after reconnection + var connectedTcs = new TaskCompletionSource(); + if (client.Status == ConnectionStatus.Connected) + connectedTcs.TrySetResult(true); + else + client.PropertyChanged += (_, args) => + { + if ( + args.PropertyName == nameof(client.Status) + && client.Status == ConnectionStatus.Connected + ) + connectedTcs.TrySetResult(true); + }; + await Task.WhenAny(connectedTcs.Task, Task.Delay(TimeoutMs)); + Assert.That( + client.Status, + Is.EqualTo(ConnectionStatus.Connected), + "Client should be connected after reconnection" + ); + + // Verify we can still send/receive after reconnection + await client.SendInstant("after-reconnect"); + + var echoResult = await Task.WhenAny(postReconnectEcho.Task, Task.Delay(TimeoutMs)); + Assert.That( + echoResult, + Is.EqualTo(postReconnectEcho.Task), + "Timed out waiting for post-reconnect echo" + ); + Assert.That(postReconnectEcho.Task.Result, Is.EqualTo("echo:after-reconnect")); + } + + // ─────────────────────────────────────────────────────────────── + // Server abort: ungraceful disconnect triggers reconnection + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task Reconnection_ServerAbort_ClientDetectsAndRecovers() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var disconnected = new TaskCompletionSource(); + var clientConnected = server.WaitForClientAsync(); + var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + IsReconnectionEnabled = true, + StateCheckInterval = TimeSpan.FromMilliseconds(50), + ReconnectTimeout = TimeSpan.FromMilliseconds(200), + ErrorReconnectTimeout = TimeSpan.FromMilliseconds(200), + Backoff = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.Zero, + MaxReconnectInterval = TimeSpan.FromMilliseconds(50), + UseJitter = false, + }, + }; + + try + { + client.Reconnecting.Subscribe(_ => + { + disconnected.TrySetResult(true); + return Task.CompletedTask; + }); + + await client.ConnectAsync(); + await clientConnected; + + // Abort = no close handshake, simulates crash + server.AbortAllClients(); + + var result = await Task.WhenAny(disconnected.Task, Task.Delay(TimeoutMs)); + // Either reconnection fires or the client detects the abort + if (result == disconnected.Task) + { + Assert.That(disconnected.Task.Result, Is.True); + } + else + { + // Server abort may not always trigger reconnect callback reliably, + // but the client should detect the disconnect + Assert.That( + client.Status, + Is.AnyOf(ConnectionStatus.Connected, ConnectionStatus.Disconnected), + "Client should have detected server abort" + ); + } + } + finally + { + try + { + await client.DisposeAsync(); + } + catch + { /* socket may already be closed */ + } + } + } + + // ─────────────────────────────────────────────────────────────── + // Graceful cleanup: dispose during active conversation + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task Dispose_DuringActiveConversation_CleansUpGracefully() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => + { + Thread.Sleep(10); // simulate slow response + return $"slow:{msg}"; + }; + server.Start(); + + var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + + // Send some messages then immediately dispose + for (int i = 0; i < 5; i++) + client.Send($"dispose-test-{i}"); + + // Dispose while messages may still be in-flight + await client.DisposeAsync(); + + // No exception means success — resources cleaned up + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Disconnected)); + } + + // ─────────────────────────────────────────────────────────────── + // Event lifecycle: Connected → Closed events bracket the session + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task Events_ConnectedAndClosed_BracketSession() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var connectedFired = new TaskCompletionSource(); + var closedFired = new TaskCompletionSource(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + client.Connected.Subscribe(_ => + { + connectedFired.TrySetResult(true); + return Task.CompletedTask; + }); + client.Closed.Subscribe(_ => + { + closedFired.TrySetResult(true); + return Task.CompletedTask; + }); + + await client.ConnectAsync(); + + var connResult = await Task.WhenAny(connectedFired.Task, Task.Delay(TimeoutMs)); + Assert.That(connResult, Is.EqualTo(connectedFired.Task), "Connected event not fired"); + + await client.CloseAsync(); + + var closeResult = await Task.WhenAny(closedFired.Task, Task.Delay(TimeoutMs)); + Assert.That(closeResult, Is.EqualTo(closedFired.Task), "Closed event not fired"); + } + + // ─────────────────────────────────────────────────────────────── + // Connection failure: invalid URL raises exception + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task ConnectionFailure_InvalidUrl_ThrowsAndReportsStatus() + { + await using var client = new WebSocketClient( + new Uri("ws://localhost:1/invalid"), + _ => Task.CompletedTask + ) + { + ConnectTimeout = TimeSpan.FromMilliseconds(500), + }; + + try + { + await client.ConnectAsync(); + Assert.Fail("Expected connection to fail"); + } + catch (Exception ex) + when (ex + is global::System.Net.WebSockets.WebSocketException + or TaskCanceledException + or OperationCanceledException + or InvalidOperationException + ) + { + // Expected + } + + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Disconnected)); + } + + // ─────────────────────────────────────────────────────────────── + // Mixed message types: text and binary in same session + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task MixedMessages_TextAndBinary_BothDelivered() + { + await using var server = new TestWebSocketServer(); + var textReceived = new TaskCompletionSource(); + var binaryReceived = new TaskCompletionSource(); + server.OnTextMessage = msg => + { + textReceived.TrySetResult(msg); + return null; + }; + server.OnBinaryMessage = data => + { + binaryReceived.TrySetResult(data); + return null; + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + + await client.SendInstant("text-msg"); + await client.SendInstant(new byte[] { 0x01, 0x02 }); + + var textResult = await Task.WhenAny(textReceived.Task, Task.Delay(TimeoutMs)); + Assert.That(textResult, Is.EqualTo(textReceived.Task), "Timed out waiting for text"); + Assert.That(textReceived.Task.Result, Is.EqualTo("text-msg")); + + var binResult = await Task.WhenAny(binaryReceived.Task, Task.Delay(TimeoutMs)); + Assert.That(binResult, Is.EqualTo(binaryReceived.Task), "Timed out waiting for binary"); + Assert.That(binaryReceived.Task.Result, Is.EqualTo(new byte[] { 0x01, 0x02 })); + } + + // ─────────────────────────────────────────────────────────────── + // Concurrent clients: multiple clients on same server + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task ConcurrentClients_MultipleClientsOnSameServer() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => $"echo:{msg}"; + server.Start(); + + const int clientCount = 5; + var tasks = new List(); + + for (int i = 0; i < clientCount; i++) + { + var idx = i; + tasks.Add( + Task.Run(async () => + { + var received = new TaskCompletionSource(); + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + received.TrySetResult(await reader.ReadToEndAsync()); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + await client.SendInstant($"client-{idx}"); + + var result = await Task.WhenAny(received.Task, Task.Delay(TimeoutMs)); + Assert.That( + result, + Is.EqualTo(received.Task), + $"Client {idx} timed out waiting for echo" + ); + Assert.That(received.Task.Result, Is.EqualTo($"echo:client-{idx}")); + + await client.CloseAsync(); + }) + ); + } + + await Task.WhenAll(tasks); + } + + // ─────────────────────────────────────────────────────────────── + // Server close: client detects server-initiated close + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task ServerClose_ClientDetectsAndFiresClosedEvent() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var closedTcs = new TaskCompletionSource(); + var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + try + { + client.Closed.Subscribe(c => closedTcs.TrySetResult(c)); + + var clientConnected = server.WaitForClientAsync(); + await client.ConnectAsync(); + await clientConnected; + + await server.CloseAllClientsAsync(); + + var result = await Task.WhenAny(closedTcs.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(closedTcs.Task), "Timed out waiting for Closed event"); + Assert.That(closedTcs.Task.Result, Is.Not.Null); + } + finally + { + try + { + await client.DisposeAsync(); + } + catch + { /* socket may already be closed */ + } + } + } + + // ─────────────────────────────────────────────────────────────── + // Large message: verify chunked transfer works + // ─────────────────────────────────────────────────────────────── + + [Test] + public async Task LargeMessage_16KB_DeliveredIntact() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => msg; // echo back + server.Start(); + + var largePayload = new string('X', 16 * 1024); // 16KB + var received = new TaskCompletionSource(); + + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + received.TrySetResult(await reader.ReadToEndAsync()); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + await client.ConnectAsync(); + await client.SendInstant(largePayload); + + var result = await Task.WhenAny(received.Task, Task.Delay(TimeoutMs)); + Assert.That(result, Is.EqualTo(received.Task), "Timed out waiting for large echo"); + Assert.That(received.Task.Result.Length, Is.EqualTo(largePayload.Length)); + Assert.That(received.Task.Result, Is.EqualTo(largePayload)); + } +} diff --git a/src/Hume.Test/Core/WebSockets/EventTests.cs b/src/Hume.Test/Core/WebSockets/EventTests.cs new file mode 100644 index 0000000..ba2ac63 --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/EventTests.cs @@ -0,0 +1,205 @@ +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class EventTests +{ + [Test] + public async Task RaiseEvent_SyncSubscriber_IsInvoked() + { + using var evt = new Event(); + string? received = null; + evt.Subscribe(s => received = s); + + await evt.RaiseEvent("hello"); + + Assert.That(received, Is.EqualTo("hello")); + } + + [Test] + public async Task RaiseEvent_AsyncSubscriber_IsInvoked() + { + using var evt = new Event(); + string? received = null; + evt.Subscribe(async s => + { + await Task.Delay(1); + received = s; + }); + + await evt.RaiseEvent("async-hello"); + + Assert.That(received, Is.EqualTo("async-hello")); + } + + [Test] + public async Task RaiseEvent_MultipleSubscribers_AllInvoked() + { + using var evt = new Event(); + var results = new List(); + evt.Subscribe(i => results.Add(i * 10)); + evt.Subscribe(i => results.Add(i * 20)); + evt.Subscribe(async i => + { + await Task.Yield(); + results.Add(i * 30); + }); + + await evt.RaiseEvent(1); + + Assert.That(results, Is.EquivalentTo(new[] { 10, 20, 30 })); + } + + [Test] + public async Task Unsubscribe_SyncHandler_NoLongerInvoked() + { + using var evt = new Event(); + var callCount = 0; + Action handler = _ => callCount++; + evt.Subscribe(handler); + + await evt.RaiseEvent("first"); + Assert.That(callCount, Is.EqualTo(1)); + + evt.Unsubscribe(handler); + await evt.RaiseEvent("second"); + Assert.That(callCount, Is.EqualTo(1)); + } + + [Test] + public async Task Unsubscribe_AsyncHandler_NoLongerInvoked() + { + using var evt = new Event(); + var callCount = 0; + Func handler = async _ => + { + await Task.Yield(); + callCount++; + }; + evt.Subscribe(handler); + + await evt.RaiseEvent("first"); + Assert.That(callCount, Is.EqualTo(1)); + + evt.Unsubscribe(handler); + await evt.RaiseEvent("second"); + Assert.That(callCount, Is.EqualTo(1)); + } + + [Test] + public async Task UnsubscribeAll_ClearsAllHandlers() + { + using var evt = new Event(); + var syncCount = 0; + var asyncCount = 0; + evt.Subscribe(_ => syncCount++); + evt.Subscribe(async _ => + { + await Task.Yield(); + asyncCount++; + }); + + await evt.RaiseEvent(1); + Assert.That(syncCount, Is.EqualTo(1)); + Assert.That(asyncCount, Is.EqualTo(1)); + + evt.UnsubscribeAll(); + await evt.RaiseEvent(2); + Assert.That(syncCount, Is.EqualTo(1)); + Assert.That(asyncCount, Is.EqualTo(1)); + } + + [Test] + public async Task Dispose_ClearsAllHandlers() + { + var evt = new Event(); + var count = 0; + evt.Subscribe(_ => count++); + + await evt.RaiseEvent(1); + Assert.That(count, Is.EqualTo(1)); + + evt.Dispose(); + await evt.RaiseEvent(2); + Assert.That(count, Is.EqualTo(1)); + } + + [Test] + public async Task RaiseEvent_NoSubscribers_DoesNotThrow() + { + using var evt = new Event(); + Assert.DoesNotThrowAsync(async () => await evt.RaiseEvent("nobody listening")); + } + + [Test] + public async Task ConcurrentSubscribeAndRaise_DoesNotThrow() + { + using var evt = new Event(); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var raiseCount = 0; + + // Start raising events on one thread + var raiser = Task.Run(async () => + { + while (!cts.Token.IsCancellationRequested) + { + await evt.RaiseEvent(Interlocked.Increment(ref raiseCount)); + await Task.Delay(1); + } + }); + + // Concurrently subscribe/unsubscribe on another thread + var subscriber = Task.Run(async () => + { + while (!cts.Token.IsCancellationRequested) + { + Action handler = _ => { }; + evt.Subscribe(handler); + await Task.Delay(1); + evt.Unsubscribe(handler); + } + }); + + // If we survive 2 seconds without exception, thread safety is OK + await Task.Delay(2000); + cts.Cancel(); + + Assert.DoesNotThrowAsync(async () => + { + await raiser; + await subscriber; + }); + Assert.That(raiseCount, Is.GreaterThan(0)); + } + + [Test] + public async Task RaiseEvent_WithComplexType_PassesCorrectData() + { + using var evt = new Event(); + Closed? received = null; + evt.Subscribe(c => received = c); + + var closed = new Closed { Code = 1000, Reason = "Normal" }; + await evt.RaiseEvent(closed); + + Assert.That(received, Is.Not.Null); + Assert.That(received!.Code, Is.EqualTo(1000)); + Assert.That(received.Reason, Is.EqualTo("Normal")); + } + + [Test] + public async Task Subscribe_SameHandlerTwice_OnlyInvokedOnce() + { + using var evt = new Event(); + var count = 0; + Action handler = _ => count++; + evt.Subscribe(handler); + evt.Subscribe(handler); // HashSet deduplicates + + await evt.RaiseEvent(1); + + Assert.That(count, Is.EqualTo(1)); + } +} diff --git a/src/Hume.Test/Core/WebSockets/QueryTests.cs b/src/Hume.Test/Core/WebSockets/QueryTests.cs new file mode 100644 index 0000000..9177ef3 --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/QueryTests.cs @@ -0,0 +1,299 @@ +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class QueryTests +{ + [Test] + public void Empty_ReturnsEmptyString() + { + var query = new Query(); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddString_ProducesQueryParam() + { + var query = new Query(); + query.Add("key", "value"); + + Assert.That(query.ToString(), Is.EqualTo("key=value")); + } + + [Test] + public void AddMultipleParams_JoinedWithAmpersand() + { + var query = new Query(); + query.Add("a", "1"); + query.Add("b", "2"); + + Assert.That(query.ToString(), Is.EqualTo("a=1&b=2")); + } + + [Test] + public void AddString_NullValue_Skipped() + { + var query = new Query(); + query.Add("key", (string?)null); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddString_EmptyValue_Skipped() + { + var query = new Query(); + query.Add("key", ""); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddString_NullKey_Skipped() + { + var query = new Query(); + query.Add(null!, "value"); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddInt_ProducesQueryParam() + { + var query = new Query(); + query.Add("count", 42); + + Assert.That(query.ToString(), Is.EqualTo("count=42")); + } + + [Test] + public void AddInt_Null_Skipped() + { + var query = new Query(); + query.Add("count", (int?)null); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddBool_ProducesLowercase() + { + var query = new Query(); + query.Add("enabled", true); + + Assert.That(query.ToString(), Is.EqualTo("enabled=true")); + } + + [Test] + public void AddBool_False() + { + var query = new Query(); + query.Add("enabled", false); + + Assert.That(query.ToString(), Is.EqualTo("enabled=false")); + } + + [Test] + public void AddBool_Null_Skipped() + { + var query = new Query(); + query.Add("enabled", (bool?)null); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddFloat_ProducesQueryParam() + { + var query = new Query(); + query.Add("rate", 1.5f); + + Assert.That(query.ToString(), Does.StartWith("rate=1.5")); + } + + [Test] + public void AddDouble_ProducesQueryParam() + { + var query = new Query(); + query.Add("pi", 3.14159); + + Assert.That(query.ToString(), Does.StartWith("pi=3.14159")); + } + + [Test] + public void AddDecimal_ProducesQueryParam() + { + var query = new Query(); + query.Add("price", 19.99m); + + Assert.That(query.ToString(), Is.EqualTo("price=19.99")); + } + + [Test] + public void AddObject_ProducesQueryParam() + { + var query = new Query(); + query.Add("obj", (object)"hello"); + + Assert.That(query.ToString(), Is.EqualTo("obj=hello")); + } + + [Test] + public void AddObject_Null_Skipped() + { + var query = new Query(); + query.Add("obj", (object?)null); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void UrlEncoding_EscapesSpecialCharacters() + { + var query = new Query(); + query.Add("name", "hello world"); + + Assert.That(query.ToString(), Is.EqualTo("name=hello%20world")); + } + + [Test] + public void UrlEncoding_EscapesAmpersandInValue() + { + var query = new Query(); + query.Add("q", "a&b"); + + Assert.That(query.ToString(), Is.EqualTo("q=a%26b")); + } + + [Test] + public void ImplicitStringConversion() + { + var query = new Query(); + query.Add("x", "1"); + + string result = query; + + Assert.That(result, Is.EqualTo("x=1")); + } + + [Test] + public void Enumerable_IteratesParameters() + { + var query = new Query(); + query.Add("a", "1"); + query.Add("b", "2"); + + var pairs = query.ToList(); + + Assert.That(pairs.Count, Is.EqualTo(2)); + Assert.That(pairs[0].Key, Is.EqualTo("a")); + Assert.That(pairs[0].Value, Is.EqualTo("1")); + Assert.That(pairs[1].Key, Is.EqualTo("b")); + Assert.That(pairs[1].Value, Is.EqualTo("2")); + } + + [Test] + public void Create_ReturnsNewInstance() + { + var query = Query.Create(); + + Assert.That(query, Is.Not.Null); + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddFloat_Null_Skipped() + { + var query = new Query(); + query.Add("rate", (float?)null); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddDouble_Null_Skipped() + { + var query = new Query(); + query.Add("pi", (double?)null); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddDecimal_Null_Skipped() + { + var query = new Query(); + query.Add("price", (decimal?)null); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddInt_EmptyKey_Skipped() + { + var query = new Query(); + query.Add("", 42); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddBool_EmptyKey_Skipped() + { + var query = new Query(); + query.Add("", true); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddFloat_EmptyKey_Skipped() + { + var query = new Query(); + query.Add("", 1.5f); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddDouble_EmptyKey_Skipped() + { + var query = new Query(); + query.Add("", 3.14); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddDecimal_EmptyKey_Skipped() + { + var query = new Query(); + query.Add("", 19.99m); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void AddObject_EmptyKey_Skipped() + { + var query = new Query(); + query.Add("", (object)"hello"); + + Assert.That(query.ToString(), Is.EqualTo("")); + } + + [Test] + public void NonGenericEnumerator_Works() + { + var query = new Query(); + query.Add("a", "1"); + + var enumerable = (global::System.Collections.IEnumerable)query; + var enumerator = enumerable.GetEnumerator(); + Assert.That(enumerator.MoveNext(), Is.True); + Assert.That(enumerator.Current, Is.InstanceOf>()); + } +} diff --git a/src/Hume.Test/Core/WebSockets/ReconnectStrategyTests.cs b/src/Hume.Test/Core/WebSockets/ReconnectStrategyTests.cs new file mode 100644 index 0000000..3bcab12 --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/ReconnectStrategyTests.cs @@ -0,0 +1,184 @@ +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class ReconnectStrategyTests +{ + [Test] + public void FirstDelay_ReturnsMinInterval() + { + var strategy = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromSeconds(1), + MaxReconnectInterval = TimeSpan.FromSeconds(60), + UseJitter = false, + }; + + var delay = strategy.GetNextDelay(); + + Assert.That(delay, Is.Not.Null); + Assert.That(delay!.Value, Is.EqualTo(TimeSpan.FromSeconds(1))); + Assert.That(strategy.AttemptsMade, Is.EqualTo(1)); + } + + [Test] + public void ExponentialBackoff_DoublesEachAttempt() + { + var strategy = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromSeconds(1), + MaxReconnectInterval = TimeSpan.FromSeconds(60), + UseJitter = false, + }; + + var d1 = strategy.GetNextDelay()!.Value; // 1s + var d2 = strategy.GetNextDelay()!.Value; // 2s + var d3 = strategy.GetNextDelay()!.Value; // 4s + var d4 = strategy.GetNextDelay()!.Value; // 8s + + Assert.That(d1.TotalSeconds, Is.EqualTo(1)); + Assert.That(d2.TotalSeconds, Is.EqualTo(2)); + Assert.That(d3.TotalSeconds, Is.EqualTo(4)); + Assert.That(d4.TotalSeconds, Is.EqualTo(8)); + } + + [Test] + public void BackoffCapsAtMax() + { + var strategy = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromSeconds(1), + MaxReconnectInterval = TimeSpan.FromSeconds(5), + UseJitter = false, + }; + + // 1, 2, 4, 5 (capped), 5 (capped) + strategy.GetNextDelay(); // 1 + strategy.GetNextDelay(); // 2 + strategy.GetNextDelay(); // 4 + var d4 = strategy.GetNextDelay()!.Value; // min(8, 5) = 5 + var d5 = strategy.GetNextDelay()!.Value; // min(10, 5) = 5 + + Assert.That(d4.TotalSeconds, Is.EqualTo(5)); + Assert.That(d5.TotalSeconds, Is.EqualTo(5)); + } + + [Test] + public void MaxAttempts_ReturnsNullWhenExhausted() + { + var strategy = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromMilliseconds(100), + MaxReconnectInterval = TimeSpan.FromSeconds(10), + MaxAttempts = 3, + UseJitter = false, + }; + + Assert.That(strategy.GetNextDelay(), Is.Not.Null); // attempt 1 + Assert.That(strategy.GetNextDelay(), Is.Not.Null); // attempt 2 + Assert.That(strategy.GetNextDelay(), Is.Not.Null); // attempt 3 + Assert.That(strategy.GetNextDelay(), Is.Null); // exhausted + Assert.That(strategy.AttemptsMade, Is.EqualTo(3)); + } + + [Test] + public void MaxAttempts_Zero_ImmediatelyExhausted() + { + var strategy = new ReconnectStrategy { MaxAttempts = 0 }; + + Assert.That(strategy.GetNextDelay(), Is.Null); + } + + [Test] + public void Reset_ClearsAttempts() + { + var strategy = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromSeconds(1), + MaxReconnectInterval = TimeSpan.FromSeconds(60), + MaxAttempts = 2, + UseJitter = false, + }; + + strategy.GetNextDelay(); // attempt 1 + strategy.GetNextDelay(); // attempt 2 + Assert.That(strategy.GetNextDelay(), Is.Null); // exhausted + + strategy.Reset(); + Assert.That(strategy.AttemptsMade, Is.EqualTo(0)); + + var delay = strategy.GetNextDelay(); + Assert.That(delay, Is.Not.Null); + Assert.That(delay!.Value, Is.EqualTo(TimeSpan.FromSeconds(1))); + } + + [Test] + public void Jitter_AddsRandomDelay() + { + var strategy = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromSeconds(10), + MaxReconnectInterval = TimeSpan.FromSeconds(60), + UseJitter = true, + }; + + var delay = strategy.GetNextDelay()!.Value; + + // With jitter, delay should be between 10s and 12.5s (10s + up to 25%) + Assert.That(delay.TotalSeconds, Is.GreaterThanOrEqualTo(10)); + Assert.That(delay.TotalSeconds, Is.LessThanOrEqualTo(12.5)); + } + + [Test] + public void Jitter_ProducesVariation() + { + // Run multiple times and verify we get at least some variation + var delays = new HashSet(); + for (int i = 0; i < 20; i++) + { + var strategy = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromSeconds(10), + MaxReconnectInterval = TimeSpan.FromSeconds(60), + UseJitter = true, + }; + delays.Add(strategy.GetNextDelay()!.Value.TotalMilliseconds); + } + + // With 20 samples, we should get more than 1 unique value + Assert.That(delays.Count, Is.GreaterThan(1)); + } + + [Test] + public void UnlimitedAttempts_NeverReturnsNull() + { + var strategy = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromMilliseconds(10), + MaxReconnectInterval = TimeSpan.FromMilliseconds(100), + MaxAttempts = null, + UseJitter = false, + }; + + for (int i = 0; i < 100; i++) + { + Assert.That(strategy.GetNextDelay(), Is.Not.Null, $"Attempt {i + 1} returned null"); + } + + Assert.That(strategy.AttemptsMade, Is.EqualTo(100)); + } + + [Test] + public void DefaultValues_AreCorrect() + { + var strategy = new ReconnectStrategy(); + + Assert.That(strategy.MinReconnectInterval, Is.EqualTo(TimeSpan.FromSeconds(1))); + Assert.That(strategy.MaxReconnectInterval, Is.EqualTo(TimeSpan.FromSeconds(60))); + Assert.That(strategy.MaxAttempts, Is.Null); + Assert.That(strategy.UseJitter, Is.True); + Assert.That(strategy.AttemptsMade, Is.EqualTo(0)); + } +} diff --git a/src/Hume.Test/Core/WebSockets/ReconnectionInfoTests.cs b/src/Hume.Test/Core/WebSockets/ReconnectionInfoTests.cs new file mode 100644 index 0000000..ada48e1 --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/ReconnectionInfoTests.cs @@ -0,0 +1,72 @@ +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class ReconnectionInfoTests +{ + [Test] + public void Constructor_SetsType() + { + var info = new ReconnectionInfo(ReconnectionType.Lost); + + Assert.That(info.Type, Is.EqualTo(ReconnectionType.Lost)); + } + + [Test] + public void Create_ReturnsNewInstance() + { + var info = ReconnectionInfo.Create(ReconnectionType.ByUser); + + Assert.That(info, Is.Not.Null); + Assert.That(info.Type, Is.EqualTo(ReconnectionType.ByUser)); + } + + [Test] + public void Create_AllReconnectionTypes() + { + foreach (ReconnectionType type in Enum.GetValues(typeof(ReconnectionType))) + { + var info = ReconnectionInfo.Create(type); + Assert.That(info.Type, Is.EqualTo(type)); + } + } + + [Test] + public void Constructor_Initial() + { + var info = new ReconnectionInfo(ReconnectionType.Initial); + Assert.That(info.Type, Is.EqualTo(ReconnectionType.Initial)); + } + + [Test] + public void Constructor_Error() + { + var info = new ReconnectionInfo(ReconnectionType.Error); + Assert.That(info.Type, Is.EqualTo(ReconnectionType.Error)); + } + + [Test] + public void Constructor_NoMessageReceived() + { + var info = new ReconnectionInfo(ReconnectionType.NoMessageReceived); + Assert.That(info.Type, Is.EqualTo(ReconnectionType.NoMessageReceived)); + } + + [Test] + public void Constructor_ByServer() + { + var info = new ReconnectionInfo(ReconnectionType.ByServer); + Assert.That(info.Type, Is.EqualTo(ReconnectionType.ByServer)); + } + + [Test] + public void Create_EquivalentToConstructor() + { + var fromCreate = ReconnectionInfo.Create(ReconnectionType.Lost); + var fromCtor = new ReconnectionInfo(ReconnectionType.Lost); + + Assert.That(fromCreate.Type, Is.EqualTo(fromCtor.Type)); + } +} diff --git a/src/Hume.Test/Core/WebSockets/RequestMessageTests.cs b/src/Hume.Test/Core/WebSockets/RequestMessageTests.cs new file mode 100644 index 0000000..f6f17a3 --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/RequestMessageTests.cs @@ -0,0 +1,71 @@ +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class RequestMessageTests +{ + [Test] + public void RequestTextMessage_StoresText() + { + var msg = new RequestTextMessage("hello world"); + + Assert.That(msg.Text, Is.EqualTo("hello world")); + Assert.That(msg, Is.InstanceOf()); + } + + [Test] + public void RequestTextMessage_EmptyString() + { + var msg = new RequestTextMessage(""); + + Assert.That(msg.Text, Is.EqualTo("")); + } + + [Test] + public void RequestBinaryMessage_StoresData() + { + var data = new byte[] { 0x01, 0x02, 0x03 }; + var msg = new RequestBinaryMessage(data); + + Assert.That(msg.Data, Is.EqualTo(data)); + Assert.That(msg, Is.InstanceOf()); + } + + [Test] + public void RequestBinaryMessage_EmptyArray() + { + var msg = new RequestBinaryMessage(Array.Empty()); + + Assert.That(msg.Data, Is.Empty); + } + + [Test] + public void RequestBinarySegmentMessage_StoresSegment() + { + var data = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 }; + var segment = new ArraySegment(data, 1, 3); + var msg = new RequestBinarySegmentMessage(segment); + + Assert.That(msg.Data.Count, Is.EqualTo(3)); + Assert.That(msg.Data[0], Is.EqualTo(0x02)); + Assert.That(msg.Data[1], Is.EqualTo(0x03)); + Assert.That(msg.Data[2], Is.EqualTo(0x04)); + Assert.That(msg, Is.InstanceOf()); + } + + [Test] + public void AllMessageTypes_DeriveFromRequestMessage() + { + RequestMessage text = new RequestTextMessage("hi"); + RequestMessage binary = new RequestBinaryMessage(new byte[] { 1 }); + RequestMessage segment = new RequestBinarySegmentMessage( + new ArraySegment(new byte[] { 1 }) + ); + + Assert.That(text, Is.InstanceOf()); + Assert.That(binary, Is.InstanceOf()); + Assert.That(segment, Is.InstanceOf()); + } +} diff --git a/src/Hume.Test/Core/WebSockets/TestWebSocketServer.cs b/src/Hume.Test/Core/WebSockets/TestWebSocketServer.cs new file mode 100644 index 0000000..897022e --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/TestWebSocketServer.cs @@ -0,0 +1,328 @@ +using global::System.Net; +using global::System.Net.WebSockets; +using global::System.Text; + +namespace Hume.Test.Core.WebSockets; + +/// +/// A lightweight in-process WebSocket server for integration tests. +/// Uses HttpListener with WebSocket support (.NET 8+). +/// +internal sealed class TestWebSocketServer : IAsyncDisposable +{ + private readonly HttpListener _listener; + private readonly CancellationTokenSource _cts = new(); + private Task? _acceptLoop; + private readonly List _connectedClients = new(); + private readonly object _clientsLock = new(); + + /// + /// Handler called for each incoming text message. Return a string to echo back, or null for no response. + /// + public Func? OnTextMessage { get; set; } + + /// + /// Handler called for each incoming binary message. Return bytes to echo back, or null for no response. + /// + public Func? OnBinaryMessage { get; set; } + + /// + /// Called when a new client connects. + /// + public Action? OnClientConnected { get; set; } + + /// + /// Called when a client disconnects. + /// + public Action? OnClientDisconnected { get; set; } + + private TaskCompletionSource? _clientConnectedTcs; + + public int Port { get; } + public Uri Uri => new($"ws://localhost:{Port}/"); + + public int ConnectedClientCount + { + get + { + lock (_clientsLock) + return _connectedClients.Count(c => c.State == WebSocketState.Open); + } + } + + public TestWebSocketServer(int port = 0) + { + Port = port == 0 ? GetAvailablePort() : port; + _listener = new HttpListener(); + _listener.Prefixes.Add($"http://localhost:{Port}/"); + } + + private static int GetAvailablePort() + { + var listener = new global::System.Net.Sockets.TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + public void Start() + { + _listener.Start(); + _acceptLoop = Task.Run(AcceptLoop); + } + + private async Task AcceptLoop() + { + try + { + while (!_cts.Token.IsCancellationRequested) + { + var context = await _listener.GetContextAsync(); + if (context.Request.IsWebSocketRequest) + { + var wsContext = await context.AcceptWebSocketAsync(null); + _ = HandleClient(wsContext.WebSocket); + } + else + { + context.Response.StatusCode = 400; + context.Response.Close(); + } + } + } + catch (HttpListenerException) when (_cts.Token.IsCancellationRequested) + { + // Expected during shutdown + } + catch (ObjectDisposedException) + { + // Expected during shutdown + } + } + + private async Task HandleClient(WebSocket ws) + { + lock (_clientsLock) + _connectedClients.Add(ws); + _clientConnectedTcs?.TrySetResult(true); + OnClientConnected?.Invoke(); + + try + { + var buffer = new byte[4096]; + while (ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested) + { + using var ms = new MemoryStream(); + WebSocketReceiveResult result; + do + { + result = await ws.ReceiveAsync(new ArraySegment(buffer), _cts.Token); + ms.Write(buffer, 0, result.Count); + } while (!result.EndOfMessage); + + if (result.MessageType == WebSocketMessageType.Close) + { + if (ws.State == WebSocketState.CloseReceived) + { + await ws.CloseOutputAsync( + WebSocketCloseStatus.NormalClosure, + "", + CancellationToken.None + ); + } + break; + } + + if (result.MessageType == WebSocketMessageType.Text) + { + var text = Encoding.UTF8.GetString(ms.ToArray()); + var response = OnTextMessage?.Invoke(text); + if (response != null) + { + var responseBytes = Encoding.UTF8.GetBytes(response); + await ws.SendAsync( + new ArraySegment(responseBytes), + WebSocketMessageType.Text, + true, + _cts.Token + ); + } + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + var data = ms.ToArray(); + var response = OnBinaryMessage?.Invoke(data); + if (response != null) + { + await ws.SendAsync( + new ArraySegment(response), + WebSocketMessageType.Binary, + true, + _cts.Token + ); + } + } + } + } + catch (WebSocketException) + { + // Client disconnected abruptly + } + catch (OperationCanceledException) + { + // Server shutting down + } + finally + { + OnClientDisconnected?.Invoke(); + } + } + + /// + /// Send a text message to all connected clients. + /// + public async Task BroadcastTextAsync(string message) + { + var bytes = Encoding.UTF8.GetBytes(message); + List clients; + lock (_clientsLock) + clients = _connectedClients.Where(c => c.State == WebSocketState.Open).ToList(); + + foreach (var client in clients) + { + try + { + await client.SendAsync( + new ArraySegment(bytes), + WebSocketMessageType.Text, + true, + _cts.Token + ); + } + catch + { + // Client may have disconnected + } + } + } + + /// + /// Send a binary message to all connected clients. + /// + public async Task BroadcastBinaryAsync(byte[] data) + { + List clients; + lock (_clientsLock) + clients = _connectedClients.Where(c => c.State == WebSocketState.Open).ToList(); + + foreach (var client in clients) + { + try + { + await client.SendAsync( + new ArraySegment(data), + WebSocketMessageType.Binary, + true, + _cts.Token + ); + } + catch + { + // Client may have disconnected + } + } + } + + /// + /// Forcibly close all connected clients (simulates server crash). + /// + public void AbortAllClients() + { + lock (_clientsLock) + { + foreach (var client in _connectedClients) + { + try + { + client.Abort(); + } + catch + { + // ignore + } + } + } + } + + /// + /// Gracefully close all connected clients with a proper WebSocket close handshake. + /// + public async Task CloseAllClientsAsync( + WebSocketCloseStatus status = WebSocketCloseStatus.NormalClosure, + string description = "Server closing" + ) + { + List clients; + lock (_clientsLock) + clients = _connectedClients.Where(c => c.State == WebSocketState.Open).ToList(); + + foreach (var client in clients) + { + try + { + await client.CloseAsync(status, description, CancellationToken.None); + } + catch + { + // Client may already be closed + } + } + } + + /// + /// Returns a task that completes when the next client connects. + /// Call this BEFORE the client connects to avoid race conditions. + /// + public Task WaitForClientAsync() + { + _clientConnectedTcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously + ); + return _clientConnectedTcs.Task; + } + + public async ValueTask DisposeAsync() + { + _cts.Cancel(); + _listener.Stop(); + if (_acceptLoop != null) + { + try + { + await _acceptLoop; + } + catch + { + // ignored + } + } + + lock (_clientsLock) + { + foreach (var client in _connectedClients) + { + try + { + client.Dispose(); + } + catch + { + // ignored + } + } + } + + _cts.Dispose(); + } +} diff --git a/src/Hume.Test/Core/WebSockets/WebSocketClientTests.cs b/src/Hume.Test/Core/WebSockets/WebSocketClientTests.cs new file mode 100644 index 0000000..9d4eba3 --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/WebSocketClientTests.cs @@ -0,0 +1,601 @@ +using global::System.Net.WebSockets; +using global::System.Text; +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class WebSocketClientTests +{ + [Test] + public async Task ConnectAsync_EstablishesConnection() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Disconnected)); + + await client.ConnectAsync(); + + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Connected)); + } + + [Test] + public async Task ConnectAsync_RaisesConnectedEvent() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + var connected = new TaskCompletionSource(); + client.Connected.Subscribe(c => connected.TrySetResult(c)); + + await client.ConnectAsync(); + + var result = await Task.WhenAny(connected.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(connected.Task)); + } + + [Test] + public async Task CloseAsync_ClosesConnection() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Connected)); + + await client.CloseAsync(); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Disconnected)); + } + + [Test] + public async Task CloseAsync_RaisesClosedEvent() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + var closed = new TaskCompletionSource(); + client.Closed.Subscribe(c => closed.TrySetResult(c)); + + await client.ConnectAsync(); + await client.CloseAsync(); + + var result = await Task.WhenAny(closed.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(closed.Task)); + } + + [Test] + public async Task SendInstant_Text_DeliversMessage() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnTextMessage = msg => + { + serverReceived.TrySetResult(msg); + return null; + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + await client.SendInstant("test-message"); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(serverReceived.Task)); + Assert.That(serverReceived.Task.Result, Is.EqualTo("test-message")); + } + + [Test] + public async Task SendInstant_Binary_DeliversMessage() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnBinaryMessage = data => + { + serverReceived.TrySetResult(data); + return null; + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + await client.SendInstant(new byte[] { 0x01, 0x02, 0x03 }); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(serverReceived.Task)); + Assert.That(serverReceived.Task.Result, Is.EqualTo(new byte[] { 0x01, 0x02, 0x03 })); + } + + [Test] + public async Task Send_QueuedText_DeliversMessage() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnTextMessage = msg => + { + serverReceived.TrySetResult(msg); + return null; + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + var queued = client.Send("queued-text"); + Assert.That(queued, Is.True); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(serverReceived.Task)); + Assert.That(serverReceived.Task.Result, Is.EqualTo("queued-text")); + } + + [Test] + public async Task Send_QueuedBinary_DeliversMessage() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnBinaryMessage = data => + { + serverReceived.TrySetResult(data); + return null; + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + var queued = client.Send(new byte[] { 0xAB, 0xCD }); + Assert.That(queued, Is.True); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(serverReceived.Task)); + Assert.That(serverReceived.Task.Result, Is.EqualTo(new byte[] { 0xAB, 0xCD })); + } + + [Test] + public void SendInstant_WhenDisconnected_Throws() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + Assert.Throws(() => + { + client.SendInstant("test").GetAwaiter().GetResult(); + }); + + client.Dispose(); + } + + [Test] + public void Send_WhenDisconnected_Throws() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + Assert.Throws(() => client.Send("test")); + + client.Dispose(); + } + + [Test] + public async Task ConnectAsync_WhenAlreadyConnected_Throws() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + + Assert.ThrowsAsync(async () => await client.ConnectAsync()); + } + + [Test] + public async Task ConnectAsync_InvalidUri_ThrowsAndSetsDisconnected() + { + await using var client = new WebSocketClient( + new Uri("ws://localhost:1/invalid"), + _ => Task.CompletedTask + ) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + try + { + await client.ConnectAsync(); + Assert.Fail("Expected connection to fail"); + } + catch (Exception ex) + when (ex + is WebSocketException + or TaskCanceledException + or OperationCanceledException + or InvalidOperationException + ) + { + // Expected: connection to invalid URI should fail + } + + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Disconnected)); + } + + [Test] + public async Task PropertyChanged_RaisedOnStatusChange() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + var statusChanges = new List(); + client.PropertyChanged += (_, args) => + { + if (args.PropertyName == nameof(client.Status)) + statusChanges.Add(client.Status.ToString()); + }; + + await client.ConnectAsync(); + await client.CloseAsync(); + + // Should have: Connecting -> Connected -> Disconnecting -> Disconnected + Assert.That(statusChanges, Does.Contain("Connecting")); + Assert.That(statusChanges, Does.Contain("Connected")); + Assert.That(statusChanges, Does.Contain("Disconnecting")); + Assert.That(statusChanges, Does.Contain("Disconnected")); + } + + [Test] + public async Task TextMessageReceived_InvokesHandler() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = _ => """{"alpha":"hello","beta":42}"""; + server.Start(); + + var received = new TaskCompletionSource(); + await using var client = new WebSocketClient( + server.Uri, + async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + received.TrySetResult(await reader.ReadToEndAsync()); + } + ) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + await client.SendInstant("trigger"); + + var result = await Task.WhenAny(received.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(received.Task)); + Assert.That(received.Task.Result, Does.Contain("alpha")); + } + + [Test] + public async Task DisposeAsync_CleansUpResources() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Connected)); + + await client.DisposeAsync(); + // After dispose, events should be cleaned up (no way to check directly, but no exception) + } + + [Test] + public async Task Dispose_Sync_CleansUpResources() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + client.Dispose(); + // No exception means success + } + + [Test] + public async Task DefaultProperties_HaveExpectedValues() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + Assert.That(client.ConnectTimeout, Is.EqualTo(TimeSpan.FromSeconds(5))); + Assert.That(client.SendTimeout, Is.EqualTo(TimeSpan.FromSeconds(30))); + Assert.That(client.IsReconnectionEnabled, Is.False); + Assert.That(client.ReconnectTimeout, Is.EqualTo(TimeSpan.FromMinutes(1))); + Assert.That(client.ErrorReconnectTimeout, Is.EqualTo(TimeSpan.FromMinutes(1))); + Assert.That(client.LostReconnectTimeout, Is.Null); + Assert.That(client.Backoff, Is.Not.Null); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Disconnected)); + Assert.That(client.HttpInvoker, Is.Null); + + client.Dispose(); + } + + [Test] + public async Task ExceptionOccurred_CanSubscribe() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + Exception? caught = null; + client.ExceptionOccurred.Subscribe(ex => caught = ex); + + // No exception should be raised during normal connection + await client.ConnectAsync(); + await Task.Delay(200); + await client.CloseAsync(); + + // We can't guarantee an exception fires, but the subscribe/unsubscribe should work + } + + [Test] + public async Task SendInstant_Memory_DeliversMessage() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnBinaryMessage = data => + { + serverReceived.TrySetResult(data); + return null; + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + await client.SendInstant((Memory)new byte[] { 0x10, 0x20 }); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(serverReceived.Task)); + Assert.That(serverReceived.Task.Result, Is.EqualTo(new byte[] { 0x10, 0x20 })); + } + + [Test] + public async Task SendInstant_ArraySegment_DeliversMessage() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnBinaryMessage = data => + { + serverReceived.TrySetResult(data); + return null; + }; + server.Start(); + + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + await client.ConnectAsync(); + await client.SendInstant(new ArraySegment(new byte[] { 0x30, 0x40 })); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(serverReceived.Task)); + Assert.That(serverReceived.Task.Result, Is.EqualTo(new byte[] { 0x30, 0x40 })); + } + + [Test] + public void SendInstant_Memory_WhenDisconnected_Throws() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + Assert.Throws(() => + { + client.SendInstant((Memory)new byte[] { 1, 2 }).GetAwaiter().GetResult(); + }); + + client.Dispose(); + } + + [Test] + public void SendInstant_ArraySegment_WhenDisconnected_Throws() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + Assert.Throws(() => + { + client + .SendInstant(new ArraySegment(new byte[] { 1, 2 })) + .GetAwaiter() + .GetResult(); + }); + + client.Dispose(); + } + + [Test] + public void Send_Binary_WhenDisconnected_Throws() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + Assert.Throws(() => client.Send(new byte[] { 1, 2 })); + + client.Dispose(); + } + + [Test] + public async Task CloseAsync_WhenNotConnected_DoesNotThrow() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + // CloseAsync when _webSocket is null should be a no-op + await client.CloseAsync(); + + client.Dispose(); + } + + [Test] + public async Task Reconnecting_Event_RaisedOnReconnection() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var reconnecting = new TaskCompletionSource(); + await using var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + IsReconnectionEnabled = true, + Backoff = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromMilliseconds(200), + MaxReconnectInterval = TimeSpan.FromSeconds(2), + UseJitter = false, + }, + }; + + client.Reconnecting.Subscribe(info => reconnecting.TrySetResult(info)); + + await client.ConnectAsync(); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Connected)); + + // Server closes connection, triggering reconnection + await server.CloseAllClientsAsync(); + + var result = await Task.WhenAny(reconnecting.Task, Task.Delay(30000)); + Assert.That( + result, + Is.EqualTo(reconnecting.Task), + "Timed out waiting for reconnecting event" + ); + Assert.That(reconnecting.Task.Result, Is.Not.Null); + + // After reconnection, status should be Connected again + await Task.Delay(500); + Assert.That(client.Status, Is.EqualTo(ConnectionStatus.Connected)); + } + + [Test] + public async Task Properties_CanBeConfigured() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + client.IsReconnectionEnabled = true; + Assert.That(client.IsReconnectionEnabled, Is.True); + + client.ReconnectTimeout = TimeSpan.FromSeconds(30); + Assert.That(client.ReconnectTimeout, Is.EqualTo(TimeSpan.FromSeconds(30))); + + client.ErrorReconnectTimeout = TimeSpan.FromSeconds(30); + Assert.That(client.ErrorReconnectTimeout, Is.EqualTo(TimeSpan.FromSeconds(30))); + + client.LostReconnectTimeout = TimeSpan.FromSeconds(5); + Assert.That(client.LostReconnectTimeout, Is.EqualTo(TimeSpan.FromSeconds(5))); + + client.ConnectTimeout = TimeSpan.FromSeconds(10); + Assert.That(client.ConnectTimeout, Is.EqualTo(TimeSpan.FromSeconds(10))); + + client.SendTimeout = TimeSpan.FromSeconds(15); + Assert.That(client.SendTimeout, Is.EqualTo(TimeSpan.FromSeconds(15))); + + client.Backoff = null; + Assert.That(client.Backoff, Is.Null); + + client.Dispose(); + } + + [Test] + public async Task DisposeAsync_WhenNotConnected_DoesNotThrow() + { + var client = new WebSocketClient(new Uri("ws://localhost:1"), _ => Task.CompletedTask); + + // Should not throw when never connected + await client.DisposeAsync(); + } + + [Test] + public async Task Closed_Event_IncludesCloseInfo() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var closed = new TaskCompletionSource(); + var client = new WebSocketClient(server.Uri, _ => Task.CompletedTask) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + }; + + client.Closed.Subscribe(c => closed.TrySetResult(c)); + + await client.ConnectAsync(); + + // Server initiates close + await server.CloseAllClientsAsync(); + + var result = await Task.WhenAny(closed.Task, Task.Delay(10000)); + Assert.That(result, Is.EqualTo(closed.Task), "Timed out waiting for close event"); + // CloseInfo should have been populated + Assert.That(closed.Task.Result, Is.Not.Null); + + // Dispose manually — the connection is already closed by server, + // so Dispose may throw if it tries to close an already-closed socket. + try + { + client.Dispose(); + } + catch + { /* already closed */ + } + } +} diff --git a/src/Hume.Test/Core/WebSockets/WebSocketConnectionTests.cs b/src/Hume.Test/Core/WebSockets/WebSocketConnectionTests.cs new file mode 100644 index 0000000..7271d8e --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/WebSocketConnectionTests.cs @@ -0,0 +1,1212 @@ +using global::System.Net.WebSockets; +using global::System.Text; +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class WebSocketConnectionTests +{ + [Test] + public async Task StartOrFail_ConnectsToServer() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + + Assert.That(ws.IsStarted, Is.True); + Assert.That(ws.IsRunning, Is.True); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task StartOrFail_InvalidUri_ThrowsException() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1/nonexistent")) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + }; + + try + { + Assert.ThrowsAsync(async () => await ws.StartOrFail()); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task TextMessageReceived_RaisesCallback() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = msg => $"echo:{msg}"; + server.Start(); + + var received = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + TextMessageReceived = async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + var text = await reader.ReadToEndAsync(); + received.TrySetResult(text); + }, + }; + + try + { + await ws.StartOrFail(); + await ws.SendInstant("hello"); + + var result = await Task.WhenAny(received.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(received.Task), "Timed out waiting for text message"); + Assert.That(received.Task.Result, Is.EqualTo("echo:hello")); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task BinaryMessageReceived_RaisesCallback() + { + await using var server = new TestWebSocketServer(); + server.OnBinaryMessage = data => data; // echo + server.Start(); + + var received = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + BinaryMessageReceived = async stream => + { + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + received.TrySetResult(ms.ToArray()); + }, + }; + + try + { + await ws.StartOrFail(); + var data = new byte[] { 0x01, 0x02, 0x03 }; + await ws.SendInstant(data); + + var result = await Task.WhenAny(received.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(received.Task), "Timed out waiting for binary message"); + Assert.That(received.Task.Result, Is.EqualTo(data)); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Stop_ClosesConnection() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + Assert.That(ws.IsRunning, Is.True); + + var stopped = await ws.Stop(WebSocketCloseStatus.NormalClosure, "done"); + Assert.That(stopped, Is.True); + Assert.That(ws.IsRunning, Is.False); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Dispose_CleansUpResources() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + await ws.StartOrFail(); + Assert.That(ws.IsRunning, Is.True); + + ws.Dispose(); + + Assert.That(ws.IsRunning, Is.False); + Assert.That(ws.IsStarted, Is.False); + } + + [Test] + public async Task Dispose_WhenAlreadyDisposed_DoesNotThrow() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + await ws.StartOrFail(); + + ws.Dispose(); + Assert.DoesNotThrow(() => ws.Dispose()); + } + + [Test] + public async Task DisconnectionHappened_RaisedOnDispose() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var disconnected = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + DisconnectionHappened = info => + { + disconnected.TrySetResult(info); + return Task.CompletedTask; + }, + }; + + await ws.StartOrFail(); + ws.Dispose(); + + var result = await Task.WhenAny(disconnected.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(disconnected.Task), "Timed out waiting for disconnection"); + Assert.That(disconnected.Task.Result.Type, Is.EqualTo(DisconnectionType.Exit)); + } + + [Test] + public async Task Send_QueuedText_IsDelivered() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnTextMessage = msg => + { + serverReceived.TrySetResult(msg); + return null; // no echo + }; + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + var queued = ws.Send("queued-message"); + Assert.That(queued, Is.True); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That( + result, + Is.EqualTo(serverReceived.Task), + "Timed out waiting for queued message" + ); + Assert.That(serverReceived.Task.Result, Is.EqualTo("queued-message")); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Send_QueuedBinary_IsDelivered() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnBinaryMessage = data => + { + serverReceived.TrySetResult(data); + return null; // no echo + }; + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + var data = new byte[] { 0xDE, 0xAD }; + var queued = ws.Send(data); + Assert.That(queued, Is.True); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That( + result, + Is.EqualTo(serverReceived.Task), + "Timed out waiting for queued binary" + ); + Assert.That(serverReceived.Task.Result, Is.EqualTo(data)); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Start_WhenAlreadyStarted_IsNoop() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + Assert.That(ws.IsStarted, Is.True); + + // Second start should be a no-op + await ws.Start(); + Assert.That(ws.IsStarted, Is.True); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public void StartOrFail_AfterDispose_ThrowsWebsocketException() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + ws.Dispose(); + + Assert.ThrowsAsync(async () => await ws.StartOrFail()); + } + + [Test] + public async Task ConnectTimeout_RespectsConfiguredValue() + { + // Use a non-routable IP address that will hang on connect + var ws = new WebSocketConnection(new Uri("ws://192.0.2.1:1")) + { + ConnectTimeout = TimeSpan.FromMilliseconds(500), + }; + + try + { + var sw = global::System.Diagnostics.Stopwatch.StartNew(); + try + { + await ws.StartOrFail(); + Assert.Fail("Expected an exception"); + } + catch (Exception ex) + when (ex + is TaskCanceledException + or OperationCanceledException + or WebSocketException + ) + { + // Expected: ConnectTimeout triggers cancellation + } + sw.Stop(); + + // Should fail within a reasonable time (timeout + margin) + Assert.That(sw.Elapsed.TotalSeconds, Is.LessThan(5)); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task DefaultProperties_HaveExpectedValues() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + try + { + Assert.That(ws.ConnectTimeout, Is.EqualTo(TimeSpan.FromSeconds(5))); + Assert.That(ws.SendTimeout, Is.EqualTo(TimeSpan.FromSeconds(30))); + Assert.That(ws.StateCheckInterval, Is.EqualTo(TimeSpan.FromSeconds(5))); + Assert.That(ws.ReconnectTimeout, Is.EqualTo(TimeSpan.FromMinutes(1))); + Assert.That(ws.ErrorReconnectTimeout, Is.EqualTo(TimeSpan.FromMinutes(1))); + Assert.That(ws.LostReconnectTimeout, Is.Null); + Assert.That(ws.SendQueueLimit, Is.EqualTo(10_000)); + Assert.That(ws.SendCacheItemTimeout, Is.EqualTo(TimeSpan.FromMinutes(30))); + Assert.That(ws.IsReconnectionEnabled, Is.False); + Assert.That(ws.IsStarted, Is.False); + Assert.That(ws.IsRunning, Is.False); + Assert.That(ws.IsTextMessageConversionEnabled, Is.True); + Assert.That(ws.IsStreamDisposedAutomatically, Is.True); + Assert.That(ws.Backoff, Is.Not.Null); + Assert.That(ws.KeepAliveTimeout, Is.EqualTo(TimeSpan.FromSeconds(20))); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task ServerInitiatedClose_ConnectionDrops() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var disconnected = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + IsReconnectionEnabled = false, + DisconnectionHappened = _ => + { + disconnected.TrySetResult(true); + return Task.CompletedTask; + }, + }; + + try + { + await ws.StartOrFail(); + Assert.That(ws.IsRunning, Is.True); + + // Server gracefully closes all connections (triggers ByServer path in Listen) + await server.CloseAllClientsAsync(); + + // Wait for the disconnection callback + var result = await Task.WhenAny(disconnected.Task, Task.Delay(10000)); + Assert.That( + result, + Is.EqualTo(disconnected.Task), + "Timed out waiting for server disconnect" + ); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task ExceptionOccurred_RaisedOnError() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var exceptionRaised = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + IsReconnectionEnabled = false, + ExceptionOccurred = ex => + { + exceptionRaised.TrySetResult(ex); + return Task.CompletedTask; + }, + }; + + try + { + await ws.StartOrFail(); + + // Abort server connections to cause an error + server.AbortAllClients(); + + var result = await Task.WhenAny(exceptionRaised.Task, Task.Delay(10000)); + // Exception may or may not fire depending on timing, but the test ensures no crash + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task SendInstant_WithCancellation_ThrowsOnCancel() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); // Cancel immediately + + // TaskCanceledException derives from OperationCanceledException + try + { + await ws.SendInstant("msg", cts.Token); + Assert.Fail("Expected cancellation exception"); + } + catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException) + { + // Expected + } + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task MultipleMessages_AllDelivered() + { + await using var server = new TestWebSocketServer(); + var messages = new List(); + var allReceived = new TaskCompletionSource(); + server.OnTextMessage = msg => + { + lock (messages) + { + messages.Add(msg); + if (messages.Count == 5) + allReceived.TrySetResult(true); + } + return null; + }; + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + + for (int i = 0; i < 5; i++) + { + await ws.SendInstant($"msg-{i}"); + } + + var result = await Task.WhenAny(allReceived.Task, Task.Delay(10000)); + Assert.That(result, Is.EqualTo(allReceived.Task), "Timed out waiting for all messages"); + + lock (messages) + { + Assert.That(messages.Count, Is.EqualTo(5)); + for (int i = 0; i < 5; i++) + Assert.That(messages, Does.Contain($"msg-{i}")); + } + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task ServerBroadcast_ReceivedByClient() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var received = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + TextMessageReceived = async stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + received.TrySetResult(await reader.ReadToEndAsync()); + }, + }; + + try + { + await ws.StartOrFail(); + + // Give a moment for server to register the client + await Task.Delay(100); + + await server.BroadcastTextAsync("server-push"); + + var result = await Task.WhenAny(received.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(received.Task)); + Assert.That(received.Task.Result, Is.EqualTo("server-push")); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Reconnection_WithBackoff_ReconnectsAfterServerClose() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var reconnected = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + IsReconnectionEnabled = true, + Backoff = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromMilliseconds(200), + MaxReconnectInterval = TimeSpan.FromSeconds(2), + UseJitter = false, + }, + ReconnectionHappened = info => + { + reconnected.TrySetResult(info); + return Task.CompletedTask; + }, + }; + + try + { + await ws.StartOrFail(); + Assert.That(ws.IsRunning, Is.True); + + // Server gracefully closes connection, triggering reconnection + await server.CloseAllClientsAsync(); + + // The server is still running so reconnection should succeed + var result = await Task.WhenAny(reconnected.Task, Task.Delay(30000)); + Assert.That(result, Is.EqualTo(reconnected.Task), "Timed out waiting for reconnection"); + + // After reconnection, should be running again + await Task.Delay(500); + Assert.That(ws.IsRunning, Is.True); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public void Constructor_WithUri_SetsUrl() + { + var uri = new Uri("ws://example.com:8080/path"); + var ws = new WebSocketConnection(uri); + try + { + Assert.That(ws.Url, Is.EqualTo(uri)); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public void Constructor_WithClientFactory_DoesNotThrow() + { + var ws = new WebSocketConnection( + new Uri("ws://localhost:1"), + clientFactory: () => new global::System.Net.WebSockets.ClientWebSocket() + ); + try + { + Assert.That(ws.Url.ToString(), Does.Contain("localhost")); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public void Name_CanBeSetAndRead() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + try + { + ws.Name = "test-client"; + Assert.That(ws.Name, Is.EqualTo("test-client")); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public void Url_CanBeChanged() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + try + { + var newUri = new Uri("ws://localhost:2"); + ws.Url = newUri; + Assert.That(ws.Url, Is.EqualTo(newUri)); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public void Properties_CanBeConfigured() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + try + { + ws.IsReconnectionEnabled = true; + Assert.That(ws.IsReconnectionEnabled, Is.True); + + ws.SendTimeout = TimeSpan.FromSeconds(10); + Assert.That(ws.SendTimeout, Is.EqualTo(TimeSpan.FromSeconds(10))); + + ws.StateCheckInterval = null; + Assert.That(ws.StateCheckInterval, Is.Null); + + ws.ReconnectTimeout = null; + Assert.That(ws.ReconnectTimeout, Is.Null); + + ws.ErrorReconnectTimeout = null; + Assert.That(ws.ErrorReconnectTimeout, Is.Null); + + ws.LostReconnectTimeout = TimeSpan.FromSeconds(3); + Assert.That(ws.LostReconnectTimeout, Is.EqualTo(TimeSpan.FromSeconds(3))); + + ws.SendQueueLimit = 0; + Assert.That(ws.SendQueueLimit, Is.EqualTo(0)); + + ws.SendCacheItemTimeout = null; + Assert.That(ws.SendCacheItemTimeout, Is.Null); + + ws.IsTextMessageConversionEnabled = false; + Assert.That(ws.IsTextMessageConversionEnabled, Is.False); + + ws.IsStreamDisposedAutomatically = false; + Assert.That(ws.IsStreamDisposedAutomatically, Is.False); + + ws.Backoff = null; + Assert.That(ws.Backoff, Is.Null); + + ws.KeepAliveInterval = TimeSpan.FromSeconds(15); + Assert.That(ws.KeepAliveInterval, Is.EqualTo(TimeSpan.FromSeconds(15))); + + ws.KeepAliveTimeout = TimeSpan.FromSeconds(10); + Assert.That(ws.KeepAliveTimeout, Is.EqualTo(TimeSpan.FromSeconds(10))); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Stop_WhenNotRunning_ReturnsFalse() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + try + { + // Not started, so Stop should return false + var result = await ws.Stop(WebSocketCloseStatus.NormalClosure, "done"); + Assert.That(result, Is.False); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public void Stop_WhenDisposed_ThrowsWebsocketException() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + ws.Dispose(); + + Assert.ThrowsAsync(async () => + await ws.Stop(WebSocketCloseStatus.NormalClosure, "done") + ); + } + + [Test] + public void StopOrFail_WhenDisposed_ThrowsWebsocketException() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + ws.Dispose(); + + Assert.ThrowsAsync(async () => + await ws.StopOrFail(WebSocketCloseStatus.NormalClosure, "done") + ); + } + + [Test] + public async Task StopOrFail_ClosesConnection() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + Assert.That(ws.IsRunning, Is.True); + + var stopped = await ws.StopOrFail(WebSocketCloseStatus.NormalClosure, "done"); + Assert.That(stopped, Is.True); + Assert.That(ws.IsRunning, Is.False); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Reconnect_WhenNotStarted_IsNoop() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + try + { + // Reconnect when not started should do nothing + await ws.Reconnect(); + Assert.That(ws.IsStarted, Is.False); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task SendInstant_ArraySegment_DeliversMessage() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnBinaryMessage = data => + { + serverReceived.TrySetResult(data); + return null; + }; + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + var data = new byte[] { 0x01, 0x02, 0x03 }; + await ws.SendInstant(new ArraySegment(data)); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That( + result, + Is.EqualTo(serverReceived.Task), + "Timed out waiting for ArraySegment message" + ); + Assert.That(serverReceived.Task.Result, Is.EqualTo(data)); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task SendInstant_Memory_DeliversMessage() + { + await using var server = new TestWebSocketServer(); + var serverReceived = new TaskCompletionSource(); + server.OnBinaryMessage = data => + { + serverReceived.TrySetResult(data); + return null; + }; + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + var data = new byte[] { 0x04, 0x05, 0x06 }; + await ws.SendInstant((Memory)data); + + var result = await Task.WhenAny(serverReceived.Task, Task.Delay(5000)); + Assert.That( + result, + Is.EqualTo(serverReceived.Task), + "Timed out waiting for Memory message" + ); + Assert.That(serverReceived.Task.Result, Is.EqualTo(data)); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task SendQueueLimit_Zero_UsesUnboundedQueue() + { + await using var server = new TestWebSocketServer(); + var messagesReceived = 0; + var allReceived = new TaskCompletionSource(); + server.OnTextMessage = _ => + { + if (Interlocked.Increment(ref messagesReceived) == 3) + allReceived.TrySetResult(true); + return null; + }; + server.Start(); + + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + SendQueueLimit = 0, // unbounded + }; + try + { + await ws.StartOrFail(); + ws.Send("msg1"); + ws.Send("msg2"); + ws.Send("msg3"); + + var result = await Task.WhenAny(allReceived.Task, Task.Delay(5000)); + Assert.That( + result, + Is.EqualTo(allReceived.Task), + "Timed out waiting for unbounded queue messages" + ); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Start_NonFailFast_DoesNotThrowOnInvalidUri() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1/nonexistent")) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + IsReconnectionEnabled = false, + Backoff = null, + ErrorReconnectTimeout = null, + }; + + try + { + // Start() (non-fail-fast) should not throw, just log and fire DisconnectionHappened + await ws.Start(); + // Wait a bit for connection attempt to fail + await Task.Delay(3000); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task DisconnectionHappened_RaisedOnStop() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var disconnected = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + DisconnectionHappened = info => + { + disconnected.TrySetResult(info); + return Task.CompletedTask; + }, + }; + + try + { + await ws.StartOrFail(); + await ws.Stop(WebSocketCloseStatus.NormalClosure, "done"); + + var result = await Task.WhenAny(disconnected.Task, Task.Delay(5000)); + Assert.That( + result, + Is.EqualTo(disconnected.Task), + "Timed out waiting for disconnection" + ); + // Stop triggers disconnection — could be ByUser or ByServer depending on timing + Assert.That( + disconnected.Task.Result.Type, + Is.AnyOf(DisconnectionType.ByUser, DisconnectionType.ByServer) + ); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Reconnection_WithBackoff_ReconnectsAfterServerAbort() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var reconnected = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + IsReconnectionEnabled = true, + // Use state check to detect aborted connection faster + StateCheckInterval = TimeSpan.FromMilliseconds(500), + Backoff = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromMilliseconds(200), + MaxReconnectInterval = TimeSpan.FromSeconds(2), + UseJitter = false, + }, + ReconnectionHappened = info => + { + reconnected.TrySetResult(info); + return Task.CompletedTask; + }, + }; + + try + { + await ws.StartOrFail(); + Assert.That(ws.IsRunning, Is.True); + + // Abort server connections (simulates crash) — the state monitor should detect this + server.AbortAllClients(); + + var result = await Task.WhenAny(reconnected.Task, Task.Delay(15000)); + if (result == reconnected.Task) + { + // Reconnected successfully + await Task.Delay(500); + Assert.That(ws.IsRunning, Is.True); + } + // If timed out, abort may not have been detected — that's OK, + // the test just verifies no crash occurs during abort + reconnect flow + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task Reconnection_MaxAttemptsExhausted_StopsReconnecting() + { + // Use a non-routable IP so connection attempts fail immediately after server close + await using var server = new TestWebSocketServer(); + server.Start(); + + var exitDisconnection = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(2), + IsReconnectionEnabled = true, + Backoff = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromMilliseconds(100), + MaxReconnectInterval = TimeSpan.FromMilliseconds(200), + MaxAttempts = 1, + UseJitter = false, + }, + DisconnectionHappened = info => + { + if (info.Type == DisconnectionType.Exit) + exitDisconnection.TrySetResult(true); + return Task.CompletedTask; + }, + }; + + try + { + await ws.StartOrFail(); + + // Point the connection to a non-routable address before triggering reconnect + // so reconnection attempts fail + ws.Url = new Uri("ws://192.0.2.1:1/"); + + // Gracefully close — triggers reconnection which will fail and exhaust max attempts + await server.CloseAllClientsAsync(); + + // Wait for exit disconnection or timeout + var result = await Task.WhenAny(exitDisconnection.Task, Task.Delay(15000)); + Assert.That( + result, + Is.EqualTo(exitDisconnection.Task), + "Timed out waiting for max attempts exhaustion" + ); + Assert.That(ws.IsStarted, Is.False); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task NativeClient_ReturnsClientWebSocket() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + try + { + await ws.StartOrFail(); + var native = ws.NativeClient; + Assert.That(native, Is.Not.Null); + Assert.That(native, Is.InstanceOf()); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public void NativeClient_BeforeStart_ReturnsNull() + { + var ws = new WebSocketConnection(new Uri("ws://localhost:1")); + try + { + // Before start, _client is null, NativeClient returns null + Assert.That(ws.NativeClient, Is.Null); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task TextMessageReceived_Null_DoesNotThrow() + { + await using var server = new TestWebSocketServer(); + server.OnTextMessage = _ => "response"; + server.Start(); + + // TextMessageReceived is null — should not throw when server sends a message + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + + try + { + await ws.StartOrFail(); + await ws.SendInstant("trigger"); + await Task.Delay(500); // Give time for response to be processed + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task BinaryMessageReceived_Null_DoesNotThrow() + { + await using var server = new TestWebSocketServer(); + server.OnBinaryMessage = _ => new byte[] { 1, 2, 3 }; + server.Start(); + + // BinaryMessageReceived is null — should not throw + var ws = new WebSocketConnection(server.Uri) { ConnectTimeout = TimeSpan.FromSeconds(5) }; + + try + { + await ws.StartOrFail(); + await ws.SendInstant(new byte[] { 0xFF }); + await Task.Delay(500); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task ServerBroadcast_Binary_ReceivedByClient() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var received = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + BinaryMessageReceived = async stream => + { + using var ms = new MemoryStream(); + await stream.CopyToAsync(ms); + received.TrySetResult(ms.ToArray()); + }, + }; + + try + { + await ws.StartOrFail(); + await Task.Delay(100); + + await server.BroadcastBinaryAsync(new byte[] { 0xAA, 0xBB }); + + var result = await Task.WhenAny(received.Task, Task.Delay(5000)); + Assert.That(result, Is.EqualTo(received.Task)); + Assert.That(received.Task.Result, Is.EqualTo(new byte[] { 0xAA, 0xBB })); + } + finally + { + ws.Dispose(); + } + } + + [Test] + public async Task LostReconnectTimeout_DelaysReconnection() + { + await using var server = new TestWebSocketServer(); + server.Start(); + + var reconnected = new TaskCompletionSource(); + var ws = new WebSocketConnection(server.Uri) + { + ConnectTimeout = TimeSpan.FromSeconds(5), + IsReconnectionEnabled = true, + LostReconnectTimeout = TimeSpan.FromMilliseconds(500), + Backoff = new ReconnectStrategy + { + MinReconnectInterval = TimeSpan.FromMilliseconds(100), + MaxReconnectInterval = TimeSpan.FromMilliseconds(200), + UseJitter = false, + }, + ReconnectionHappened = info => + { + reconnected.TrySetResult(info); + return Task.CompletedTask; + }, + }; + + try + { + await ws.StartOrFail(); + var sw = global::System.Diagnostics.Stopwatch.StartNew(); + + // Use graceful close — more reliable than abort for triggering reconnection + await server.CloseAllClientsAsync(); + + var result = await Task.WhenAny(reconnected.Task, Task.Delay(30000)); + sw.Stop(); + Assert.That( + result, + Is.EqualTo(reconnected.Task), + "Timed out waiting for delayed reconnection" + ); + + // Should have waited at least the LostReconnectTimeout + Assert.That(sw.Elapsed.TotalMilliseconds, Is.GreaterThan(400)); + } + finally + { + ws.Dispose(); + } + } +} diff --git a/src/Hume.Test/Core/WebSockets/WebsocketExceptionTests.cs b/src/Hume.Test/Core/WebSockets/WebsocketExceptionTests.cs new file mode 100644 index 0000000..1d07e93 --- /dev/null +++ b/src/Hume.Test/Core/WebSockets/WebsocketExceptionTests.cs @@ -0,0 +1,75 @@ +using Hume.Core.WebSockets; +using NUnit.Framework; + +namespace Hume.Test.Core.WebSockets; + +[TestFixture] +public class WebsocketExceptionTests +{ + [Test] + public void DefaultConstructor_CreatesException() + { + var ex = new WebsocketException(); + + Assert.That(ex, Is.InstanceOf()); + Assert.That(ex.Message, Is.Not.Null); + Assert.That(ex.InnerException, Is.Null); + } + + [Test] + public void MessageConstructor_SetsMessage() + { + var ex = new WebsocketException("connection failed"); + + Assert.That(ex.Message, Is.EqualTo("connection failed")); + Assert.That(ex.InnerException, Is.Null); + } + + [Test] + public void MessageAndInnerException_SetsBoth() + { + var inner = new InvalidOperationException("inner error"); + var ex = new WebsocketException("outer error", inner); + + Assert.That(ex.Message, Is.EqualTo("outer error")); + Assert.That(ex.InnerException, Is.SameAs(inner)); + Assert.That(ex.InnerException!.Message, Is.EqualTo("inner error")); + } + + [Test] + public void IsException_DerivedFromSystemException() + { + var ex = new WebsocketException("test"); + + Assert.That(ex, Is.InstanceOf()); + Assert.That(ex, Is.InstanceOf()); + } + + [Test] + public void CanBeCaughtAsException() + { + try + { + throw new WebsocketException("thrown"); + } + catch (Exception ex) + { + Assert.That(ex, Is.InstanceOf()); + Assert.That(ex.Message, Is.EqualTo("thrown")); + } + } + + [Test] + public void CanBeCaughtAsWebsocketException() + { + try + { + throw new WebsocketException("specific", new TimeoutException("timeout")); + } + catch (WebsocketException ex) + { + Assert.That(ex.Message, Is.EqualTo("specific")); + Assert.That(ex.InnerException, Is.InstanceOf()); + } + } +} diff --git a/src/Hume.Test/Hume.Test.csproj b/src/Hume.Test/Hume.Test.csproj index e6397a2..209d353 100644 --- a/src/Hume.Test/Hume.Test.csproj +++ b/src/Hume.Test/Hume.Test.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 12 enable enable @@ -14,18 +14,18 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Hume.Test/Unit/MockServer/BaseMockServerTest.cs b/src/Hume.Test/Unit/MockServer/BaseMockServerTest.cs index 8d9d2ef..1d4c8cb 100644 --- a/src/Hume.Test/Unit/MockServer/BaseMockServerTest.cs +++ b/src/Hume.Test/Unit/MockServer/BaseMockServerTest.cs @@ -6,14 +6,13 @@ namespace Hume.Test.Unit.MockServer; -[SetUpFixture] public class BaseMockServerTest { - protected static WireMockServer Server { get; set; } = null!; + protected WireMockServer Server { get; set; } = null!; - protected static HumeClient Client { get; set; } = null!; + protected HumeClient Client { get; set; } = null!; - protected static RequestOptions RequestOptions { get; set; } = new(); + protected RequestOptions RequestOptions { get; set; } = new(); [OneTimeSetUp] public void GlobalSetup() diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetAudioTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/GetAudioTest.cs similarity index 92% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/GetAudioTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/GetAudioTest.cs index 7787c57..23ea88b 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetAudioTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/GetAudioTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.ChatGroups; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetAudioTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetChatGroupTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/GetChatGroupTest.cs similarity index 92% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/GetChatGroupTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/GetChatGroupTest.cs index 396f214..2d12709 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetChatGroupTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/GetChatGroupTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.ChatGroups; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetChatGroupTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatGroupEventsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/ListChatGroupEventsTest.cs similarity index 99% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatGroupEventsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/ListChatGroupEventsTest.cs index 196c7f1..4fb8f14 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatGroupEventsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/ListChatGroupEventsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.ChatGroups; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListChatGroupEventsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatGroupsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/ListChatGroupsTest.cs similarity index 91% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatGroupsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/ListChatGroupsTest.cs index 8d8a044..9514dc7 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatGroupsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ChatGroups/ListChatGroupsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.ChatGroups; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListChatGroupsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetAudioTest_.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/GetAudioTest.cs similarity index 85% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/GetAudioTest_.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/GetAudioTest.cs index a93c995..8379546 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetAudioTest_.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/GetAudioTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Chats; [TestFixture] -public class GetAudioTest_ : BaseMockServerTest +[Parallelizable(ParallelScope.Self)] +public class GetAudioTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatEventsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/ListChatEventsTest.cs similarity index 99% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatEventsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/ListChatEventsTest.cs index 586fff3..0ddb004 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatEventsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/ListChatEventsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Chats; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListChatEventsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/ListChatsTest.cs similarity index 92% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/ListChatsTest.cs index 63ae21d..69aca0f 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListChatsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Chats/ListChatsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Chats; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListChatsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateConfigTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/CreateConfigTest.cs similarity index 97% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateConfigTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/CreateConfigTest.cs index d84e164..7e0436c 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateConfigTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/CreateConfigTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CreateConfigTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateConfigVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/CreateConfigVersionTest.cs similarity index 97% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateConfigVersionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/CreateConfigVersionTest.cs index ecda49b..4f72ce1 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateConfigVersionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/CreateConfigVersionTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CreateConfigVersionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteConfigTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/DeleteConfigTest.cs similarity index 87% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteConfigTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/DeleteConfigTest.cs index deef26f..ba70877 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteConfigTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/DeleteConfigTest.cs @@ -1,9 +1,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DeleteConfigTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteConfigVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/DeleteConfigVersionTest.cs similarity index 87% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteConfigVersionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/DeleteConfigVersionTest.cs index 749bbad..fa98cb8 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteConfigVersionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/DeleteConfigVersionTest.cs @@ -1,9 +1,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DeleteConfigVersionTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetConfigVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/GetConfigVersionTest.cs similarity index 95% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/GetConfigVersionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/GetConfigVersionTest.cs index e0e267c..7758d2d 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetConfigVersionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/GetConfigVersionTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetConfigVersionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListConfigVersionsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/ListConfigVersionsTest.cs similarity index 95% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListConfigVersionsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/ListConfigVersionsTest.cs index 3c9ca12..a35134b 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListConfigVersionsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/ListConfigVersionsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListConfigVersionsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListConfigsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/ListConfigsTest.cs similarity index 95% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListConfigsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/ListConfigsTest.cs index 0426c30..6e5ca13 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListConfigsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/ListConfigsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListConfigsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateConfigDescriptionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/UpdateConfigDescriptionTest.cs similarity index 95% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateConfigDescriptionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/UpdateConfigDescriptionTest.cs index d3ff1d7..421e7e5 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateConfigDescriptionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/UpdateConfigDescriptionTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class UpdateConfigDescriptionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateConfigNameTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/UpdateConfigNameTest.cs similarity index 91% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateConfigNameTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/UpdateConfigNameTest.cs index ab7303b..46cc894 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateConfigNameTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Configs/UpdateConfigNameTest.cs @@ -2,9 +2,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Configs; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class UpdateConfigNameTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/SendTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ControlPlane/SendTest.cs similarity index 78% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/SendTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/ControlPlane/SendTest.cs index 0d67163..95b14e7 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/SendTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/ControlPlane/SendTest.cs @@ -2,9 +2,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.ControlPlane; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class SendTest : BaseMockServerTest { [NUnit.Framework.Test] @@ -28,10 +29,7 @@ public void MockServerTest() .RespondWith(WireMock.ResponseBuilders.Response.Create().WithStatusCode(200)); Assert.DoesNotThrowAsync(async () => - await Client.EmpathicVoice.ControlPlane.SendAsync( - "chat_id", - new SessionSettings { Type = "session_settings" } - ) + await Client.EmpathicVoice.ControlPlane.SendAsync("chat_id", new SessionSettings()) ); } } diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteToolVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteToolVersionTest.cs deleted file mode 100644 index a817242..0000000 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteToolVersionTest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Hume.Test.Unit.MockServer; -using NUnit.Framework; - -namespace Hume.Test.Unit.MockServer.EmpathicVoice; - -[TestFixture] -public class DeleteToolVersionTest : BaseMockServerTest -{ - [NUnit.Framework.Test] - public void MockServerTest() - { - Server - .Given( - WireMock - .RequestBuilders.Request.Create() - .WithPath("/v0/evi/tools//version/1") - .UsingDelete() - ) - .RespondWith(WireMock.ResponseBuilders.Response.Create().WithStatusCode(200)); - - Assert.DoesNotThrowAsync(async () => - await Client.EmpathicVoice.Tools.DeleteToolVersionAsync("", 1) - ); - } -} diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreatePromptTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/CreatePromptTest.cs similarity index 93% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/CreatePromptTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/CreatePromptTest.cs index 9067ec4..26e07a6 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreatePromptTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/CreatePromptTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CreatePromptTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreatePromptVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/CreatePromptVersionTest.cs similarity index 94% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/CreatePromptVersionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/CreatePromptVersionTest.cs index a2ca72d..88c6b9b 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreatePromptVersionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/CreatePromptVersionTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CreatePromptVersionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeletePromptTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/DeletePromptTest.cs similarity index 87% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/DeletePromptTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/DeletePromptTest.cs index 505547d..5aa56f3 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeletePromptTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/DeletePromptTest.cs @@ -1,9 +1,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DeletePromptTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeletePromptVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/DeletePromptVersionTest.cs similarity index 87% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/DeletePromptVersionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/DeletePromptVersionTest.cs index 65d9e04..e1bef44 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeletePromptVersionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/DeletePromptVersionTest.cs @@ -1,9 +1,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DeletePromptVersionTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetPromptVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/GetPromptVersionTest.cs similarity index 90% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/GetPromptVersionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/GetPromptVersionTest.cs index 2d6cc75..b866425 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetPromptVersionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/GetPromptVersionTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetPromptVersionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListPromptVersionsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/ListPromptVersionsTest.cs similarity index 91% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListPromptVersionsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/ListPromptVersionsTest.cs index a01eeea..b207be9 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListPromptVersionsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/ListPromptVersionsTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListPromptVersionsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListPromptsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/ListPromptsTest.cs similarity index 94% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListPromptsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/ListPromptsTest.cs index b870f13..b3d0b46 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListPromptsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/ListPromptsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListPromptsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdatePromptDescriptionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/UpdatePromptDescriptionTest.cs similarity index 92% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdatePromptDescriptionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/UpdatePromptDescriptionTest.cs index ae71317..6c5997b 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdatePromptDescriptionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/UpdatePromptDescriptionTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class UpdatePromptDescriptionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdatePromptNameTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/UpdatePromptNameTest.cs similarity index 91% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdatePromptNameTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/UpdatePromptNameTest.cs index af86609..0ecd44f 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdatePromptNameTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Prompts/UpdatePromptNameTest.cs @@ -2,9 +2,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Prompts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class UpdatePromptNameTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateToolTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/CreateToolTest.cs similarity index 95% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateToolTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/CreateToolTest.cs index 5b58cc0..a4c7e5e 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateToolTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/CreateToolTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Tools; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CreateToolTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateToolVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/CreateToolVersionTest.cs similarity index 95% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateToolVersionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/CreateToolVersionTest.cs index 5d9845a..fd087ba 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/CreateToolVersionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/CreateToolVersionTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Tools; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CreateToolVersionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteToolTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/DeleteToolTest.cs similarity index 87% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteToolTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/DeleteToolTest.cs index fb73043..53860cc 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/DeleteToolTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/DeleteToolTest.cs @@ -1,9 +1,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Tools; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DeleteToolTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetToolVersionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/GetToolVersionTest.cs similarity index 91% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/GetToolVersionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/GetToolVersionTest.cs index f619da6..bdb4663 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/GetToolVersionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/GetToolVersionTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Tools; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetToolVersionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListToolVersionsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/ListToolVersionsTest.cs similarity index 93% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListToolVersionsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/ListToolVersionsTest.cs index 285e012..d6107f7 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListToolVersionsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/ListToolVersionsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Tools; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListToolVersionsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListToolsTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/ListToolsTest.cs similarity index 94% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/ListToolsTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/ListToolsTest.cs index d64a68a..daf9e74 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/ListToolsTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/ListToolsTest.cs @@ -2,13 +2,14 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Tools; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListToolsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateToolDescriptionTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/UpdateToolDescriptionTest.cs similarity index 94% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateToolDescriptionTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/UpdateToolDescriptionTest.cs index de57a99..b872f5c 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateToolDescriptionTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/UpdateToolDescriptionTest.cs @@ -3,13 +3,14 @@ using Hume.Test.Utils; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Tools; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class UpdateToolDescriptionTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateToolNameTest.cs b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/UpdateToolNameTest.cs similarity index 91% rename from src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateToolNameTest.cs rename to src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/UpdateToolNameTest.cs index 491435d..6612cf0 100644 --- a/src/Hume.Test/Unit/MockServer/EmpathicVoice/UpdateToolNameTest.cs +++ b/src/Hume.Test/Unit/MockServer/EmpathicVoice/Tools/UpdateToolNameTest.cs @@ -2,9 +2,10 @@ using Hume.Test.Unit.MockServer; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.EmpathicVoice; +namespace Hume.Test.Unit.MockServer.EmpathicVoice.Tools; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class UpdateToolNameTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/GetJobDetailsTest.cs b/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/GetJobDetailsTest.cs index 83233cf..d63fdc0 100644 --- a/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/GetJobDetailsTest.cs +++ b/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/GetJobDetailsTest.cs @@ -5,10 +5,11 @@ namespace Hume.Test.Unit.MockServer.ExpressionMeasurement.Batch; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetJobDetailsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/GetJobPredictionsTest.cs b/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/GetJobPredictionsTest.cs index b7e2f86..a011a8f 100644 --- a/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/GetJobPredictionsTest.cs +++ b/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/GetJobPredictionsTest.cs @@ -5,10 +5,11 @@ namespace Hume.Test.Unit.MockServer.ExpressionMeasurement.Batch; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class GetJobPredictionsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ [ diff --git a/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/ListJobsTest.cs b/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/ListJobsTest.cs index edb2ec9..f98f1a1 100644 --- a/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/ListJobsTest.cs +++ b/src/Hume.Test/Unit/MockServer/ExpressionMeasurement/Batch/ListJobsTest.cs @@ -6,10 +6,11 @@ namespace Hume.Test.Unit.MockServer.ExpressionMeasurement.Batch; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListJobsTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ [ diff --git a/src/Hume.Test/Unit/MockServer/Tts/SynthesizeJsonStreamingTest.cs b/src/Hume.Test/Unit/MockServer/Tts/SynthesizeJsonStreamingTest.cs index ad7a308..056e04b 100644 --- a/src/Hume.Test/Unit/MockServer/Tts/SynthesizeJsonStreamingTest.cs +++ b/src/Hume.Test/Unit/MockServer/Tts/SynthesizeJsonStreamingTest.cs @@ -5,6 +5,7 @@ namespace Hume.Test.Unit.MockServer.Tts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class SynthesizeJsonStreamingTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/Tts/SynthesizeJsonTest.cs b/src/Hume.Test/Unit/MockServer/Tts/SynthesizeJsonTest.cs index 35f5c0a..ba68795 100644 --- a/src/Hume.Test/Unit/MockServer/Tts/SynthesizeJsonTest.cs +++ b/src/Hume.Test/Unit/MockServer/Tts/SynthesizeJsonTest.cs @@ -6,10 +6,11 @@ namespace Hume.Test.Unit.MockServer.Tts; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class SynthesizeJsonTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { @@ -95,7 +96,7 @@ public async System.Threading.Tasks.Task MockServerTest() }, }, }, - Format = new FormatMp3 { Type = "mp3" }, + Format = new FormatMp3(), NumGenerations = 1, Utterances = new List() { diff --git a/src/Hume.Test/Unit/MockServer/Tts/CreateTest.cs b/src/Hume.Test/Unit/MockServer/Tts/Voices/CreateTest.cs similarity index 89% rename from src/Hume.Test/Unit/MockServer/Tts/CreateTest.cs rename to src/Hume.Test/Unit/MockServer/Tts/Voices/CreateTest.cs index 407c17d..7f57fd1 100644 --- a/src/Hume.Test/Unit/MockServer/Tts/CreateTest.cs +++ b/src/Hume.Test/Unit/MockServer/Tts/Voices/CreateTest.cs @@ -3,13 +3,14 @@ using Hume.Tts; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.Tts; +namespace Hume.Test.Unit.MockServer.Tts.Voices; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class CreateTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string requestJson = """ { diff --git a/src/Hume.Test/Unit/MockServer/Tts/DeleteTest.cs b/src/Hume.Test/Unit/MockServer/Tts/Voices/DeleteTest.cs similarity index 89% rename from src/Hume.Test/Unit/MockServer/Tts/DeleteTest.cs rename to src/Hume.Test/Unit/MockServer/Tts/Voices/DeleteTest.cs index bf51b2e..02186da 100644 --- a/src/Hume.Test/Unit/MockServer/Tts/DeleteTest.cs +++ b/src/Hume.Test/Unit/MockServer/Tts/Voices/DeleteTest.cs @@ -2,9 +2,10 @@ using Hume.Tts; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.Tts; +namespace Hume.Test.Unit.MockServer.Tts.Voices; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class DeleteTest : BaseMockServerTest { [NUnit.Framework.Test] diff --git a/src/Hume.Test/Unit/MockServer/Tts/ListTest.cs b/src/Hume.Test/Unit/MockServer/Tts/Voices/ListTest.cs similarity index 90% rename from src/Hume.Test/Unit/MockServer/Tts/ListTest.cs rename to src/Hume.Test/Unit/MockServer/Tts/Voices/ListTest.cs index c6a7da1..29a4c45 100644 --- a/src/Hume.Test/Unit/MockServer/Tts/ListTest.cs +++ b/src/Hume.Test/Unit/MockServer/Tts/Voices/ListTest.cs @@ -2,13 +2,14 @@ using Hume.Tts; using NUnit.Framework; -namespace Hume.Test.Unit.MockServer.Tts; +namespace Hume.Test.Unit.MockServer.Tts.Voices; [TestFixture] +[Parallelizable(ParallelScope.Self)] public class ListTest : BaseMockServerTest { [NUnit.Framework.Test] - public async System.Threading.Tasks.Task MockServerTest() + public async global::System.Threading.Tasks.Task MockServerTest() { const string mockResponse = """ { diff --git a/src/Hume.Test/Utils/AdditionalPropertiesComparer.cs b/src/Hume.Test/Utils/AdditionalPropertiesComparer.cs index 52ded15..9917dc8 100644 --- a/src/Hume.Test/Utils/AdditionalPropertiesComparer.cs +++ b/src/Hume.Test/Utils/AdditionalPropertiesComparer.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; using NUnit.Framework.Constraints; @@ -51,6 +51,50 @@ public static EqualConstraint UsingAdditionalPropertiesComparer(this EqualConstr return constraint; } + /// + /// Modifies the EqualConstraint to handle Dictionary<string, object?> values by comparing + /// their serialized JSON representations. This handles the type mismatch between native C# types + /// and JsonElement values that occur when comparing manually constructed objects with + /// deserialized objects. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingObjectDictionaryComparer(this EqualConstraint constraint) + { + constraint.Using>( + (x, y) => + { + if (x.Count != y.Count) + { + return false; + } + + foreach (var key in x.Keys) + { + if (!y.ContainsKey(key)) + { + return false; + } + + var xElement = JsonUtils.SerializeToElement(x[key]); + var yElement = JsonUtils.SerializeToElement(y[key]); + + if (!JsonElementsAreEqual(xElement, yElement)) + { + return false; + } + } + + return true; + } + ); + + return constraint; + } + + internal static bool JsonElementsAreEqualPublic(JsonElement x, JsonElement y) => + JsonElementsAreEqual(x, y); + private static bool JsonElementsAreEqual(JsonElement x, JsonElement y) { if (x.ValueKind != y.ValueKind) @@ -123,4 +167,53 @@ private static bool CompareJsonArrays(JsonElement x, JsonElement y) return true; } + + /// + /// Modifies the EqualConstraint to handle cross-type comparisons involving JsonElement. + /// When UsingPropertiesComparer() walks object properties and encounters a property typed as + /// 'object', the expected side may be a Dictionary<object, object?> while the actual + /// (deserialized) side is a JsonElement. These typed predicates bridge that gap by serializing + /// the non-JsonElement side and comparing JSON representations. + /// + /// Uses typed Func<TExpected, TActual, bool> predicates instead of a non-generic + /// IComparer/IEqualityComparer so that NUnit's CanCompare type check ensures these only + /// fire when one side is a JsonElement, letting UsingPropertiesComparer() handle all + /// same-type comparisons normally. + /// + /// The EqualConstraint to modify. + /// The same constraint instance for method chaining. + public static EqualConstraint UsingJsonSerializationComparer(this EqualConstraint constraint) + { + // Handle: expected is non-JsonElement, actual is JsonElement + constraint.Using( + (actualJsonElement, expectedObj) => + { + try + { + var expectedElement = JsonUtils.SerializeToElement(expectedObj); + return JsonElementsAreEqualPublic(expectedElement, actualJsonElement); + } + catch + { + return false; + } + } + ); + // Handle reverse: expected is JsonElement, actual is non-JsonElement + constraint.Using( + (actualObj, expectedJsonElement) => + { + try + { + var actualElement = JsonUtils.SerializeToElement(actualObj); + return JsonElementsAreEqualPublic(expectedJsonElement, actualElement); + } + catch + { + return false; + } + } + ); + return constraint; + } } diff --git a/src/Hume.Test/Utils/JsonAssert.cs b/src/Hume.Test/Utils/JsonAssert.cs index 9df3cbf..47ee5e7 100644 --- a/src/Hume.Test/Utils/JsonAssert.cs +++ b/src/Hume.Test/Utils/JsonAssert.cs @@ -16,4 +16,14 @@ internal static void AreEqual(object actual, string expectedJson) var expectedElement = JsonUtils.Deserialize(expectedJson); Assert.That(actualElement, Is.EqualTo(expectedElement).UsingJsonElementComparer()); } + + /// + /// Asserts that the given JSON string survives a deserialization/serialization round-trip + /// intact: deserializes to T then re-serializes and compares to the original JSON. + /// + internal static void Roundtrips(string json) + { + var deserialized = JsonUtils.Deserialize(json); + AreEqual(deserialized!, json); + } } diff --git a/src/Hume.Test/Utils/JsonElementComparer.cs b/src/Hume.Test/Utils/JsonElementComparer.cs index 404e3c0..36fa0e9 100644 --- a/src/Hume.Test/Utils/JsonElementComparer.cs +++ b/src/Hume.Test/Utils/JsonElementComparer.cs @@ -90,14 +90,17 @@ private bool CompareJsonElements(JsonElement x, JsonElement y, string path) return false; case JsonValueKind.Number: - // Use double comparison with tolerance for floating-point precision - var xDouble = x.GetDouble(); - var yDouble = y.GetDouble(); - const double epsilon = 1e-10; - if (global::System.Math.Abs(xDouble - yDouble) > epsilon) + if (x.GetDecimal() != y.GetDecimal()) { - _failurePath = $"{path}: Expected {xDouble} but got {yDouble}"; - return false; + if (x.GetDouble() != y.GetDouble()) + { + if (x.GetSingle() != y.GetSingle()) + { + _failurePath = + $"{path}: Expected {x.GetDecimal()} but got {y.GetDecimal()}"; + return false; + } + } } return true; diff --git a/src/Hume.Test/Utils/NUnitExtensions.cs b/src/Hume.Test/Utils/NUnitExtensions.cs index 170795c..816f4c0 100644 --- a/src/Hume.Test/Utils/NUnitExtensions.cs +++ b/src/Hume.Test/Utils/NUnitExtensions.cs @@ -26,5 +26,7 @@ public static EqualConstraint UsingDefaults(this EqualConstraint constraint) => .UsingOneOfComparer() .UsingJsonElementComparer() .UsingOptionalComparer() - .UsingAdditionalPropertiesComparer(); + .UsingObjectDictionaryComparer() + .UsingAdditionalPropertiesComparer() + .UsingJsonSerializationComparer(); } diff --git a/src/Hume/Core/BaseRequest.cs b/src/Hume/Core/BaseRequest.cs index 1bcbc41..50cf538 100644 --- a/src/Hume/Core/BaseRequest.cs +++ b/src/Hume/Core/BaseRequest.cs @@ -6,7 +6,7 @@ namespace Hume.Core; internal abstract record BaseRequest { - internal required string BaseUrl { get; init; } + internal string? BaseUrl { get; init; } internal required HttpMethod Method { get; init; } diff --git a/src/Hume/Core/CollectionItemSerializer.cs b/src/Hume/Core/CollectionItemSerializer.cs index e956056..8a0ca44 100644 --- a/src/Hume/Core/CollectionItemSerializer.cs +++ b/src/Hume/Core/CollectionItemSerializer.cs @@ -10,8 +10,10 @@ namespace Hume.Core; /// Converter to use for individual items. internal class CollectionItemSerializer : JsonConverter> - where TConverterType : JsonConverter + where TConverterType : JsonConverter, new() { + private static readonly TConverterType _converter = new TConverterType(); + /// /// Reads a json string and deserializes it into an object. /// @@ -32,7 +34,7 @@ JsonSerializerOptions options var jsonSerializerOptions = new JsonSerializerOptions(options); jsonSerializerOptions.Converters.Clear(); - jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + jsonSerializerOptions.Converters.Add(_converter); var returnValue = new List(); @@ -75,7 +77,7 @@ JsonSerializerOptions options var jsonSerializerOptions = new JsonSerializerOptions(options); jsonSerializerOptions.Converters.Clear(); - jsonSerializerOptions.Converters.Add(Activator.CreateInstance()); + jsonSerializerOptions.Converters.Add(_converter); writer.WriteStartArray(); diff --git a/src/Hume/Core/DateTimeSerializer.cs b/src/Hume/Core/DateTimeSerializer.cs index 2f89cce..2d4be36 100644 --- a/src/Hume/Core/DateTimeSerializer.cs +++ b/src/Hume/Core/DateTimeSerializer.cs @@ -19,4 +19,22 @@ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializer { writer.WriteStringValue(value.ToString(Constants.DateTimeFormat)); } + + public override DateTime ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + return DateTime.Parse(reader.GetString()!, null, DateTimeStyles.RoundtripKind); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + DateTime value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.ToString(Constants.DateTimeFormat)); + } } diff --git a/src/Hume/Core/HeaderValue.cs b/src/Hume/Core/HeaderValue.cs index c5fbf6f..22227c7 100644 --- a/src/Hume/Core/HeaderValue.cs +++ b/src/Hume/Core/HeaderValue.cs @@ -2,26 +2,27 @@ namespace Hume.Core; internal sealed class HeaderValue { - private readonly object _value; - private readonly string _type; + private readonly Func> _resolver; - private HeaderValue(object value, string type) + public HeaderValue(string value) { - _value = value; - _type = type; + _resolver = () => new global::System.Threading.Tasks.ValueTask(value); } - public HeaderValue(string value) - : this(value, "string") { } - public HeaderValue(Func value) - : this(value, "func") { } + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value()); + } public HeaderValue(Func> value) - : this(value, "valueTask") { } + { + _resolver = value; + } public HeaderValue(Func> value) - : this(value, "task") { } + { + _resolver = () => new global::System.Threading.Tasks.ValueTask(value()); + } public static implicit operator HeaderValue(string value) => new(value); @@ -47,21 +48,5 @@ public static HeaderValue FromTaskFunc( Func> value ) => new(value); - internal global::System.Threading.Tasks.ValueTask ResolveAsync() - { - return _type switch - { - "string" => new global::System.Threading.Tasks.ValueTask((string)_value), - "func" => new global::System.Threading.Tasks.ValueTask( - ((Func)_value)() - ), - "valueTask" => ((Func>)_value)(), - "task" => new global::System.Threading.Tasks.ValueTask( - ((Func>)_value)() - ), - _ => throw new global::System.InvalidOperationException( - $"Unknown header value type: {_type}" - ), - }; - } + internal global::System.Threading.Tasks.ValueTask ResolveAsync() => _resolver(); } diff --git a/src/Hume/Core/HttpContentExtensions.cs b/src/Hume/Core/HttpContentExtensions.cs new file mode 100644 index 0000000..583b904 --- /dev/null +++ b/src/Hume/Core/HttpContentExtensions.cs @@ -0,0 +1,20 @@ +#if !NET5_0_OR_GREATER +namespace Hume.Core; + +/// +/// Polyfill extension providing a ReadAsStringAsync(CancellationToken) overload +/// for target frameworks older than .NET 5, where only the parameterless +/// ReadAsStringAsync() is available. +/// +internal static class HttpContentExtensions +{ + internal static Task ReadAsStringAsync( + this HttpContent httpContent, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return httpContent.ReadAsStringAsync(); + } +} +#endif diff --git a/src/Hume/Core/IRequestOptions.cs b/src/Hume/Core/IRequestOptions.cs index 494cc30..9c2def6 100644 --- a/src/Hume/Core/IRequestOptions.cs +++ b/src/Hume/Core/IRequestOptions.cs @@ -37,7 +37,7 @@ internal interface IRequestOptions } /// - /// The http client used to make requests. + /// The max number of retries to attempt. /// public int? MaxRetries { get; #if NET5_0_OR_GREATER diff --git a/src/Hume/Core/JsonConfiguration.cs b/src/Hume/Core/JsonConfiguration.cs index a311a49..4adfb79 100644 --- a/src/Hume/Core/JsonConfiguration.cs +++ b/src/Hume/Core/JsonConfiguration.cs @@ -308,7 +308,7 @@ internal static bool TryDeserialize(JsonDocument json, out T? result) { try { - result = json.Deserialize(); + result = json.Deserialize(JsonOptions.JsonSerializerOptions); return true; } catch (global::System.Exception) @@ -323,7 +323,7 @@ internal static bool TryDeserialize(JsonDocument json, out T? result) { try { - result = json.Deserialize(); + result = json.Deserialize(JsonOptions.JsonSerializerOptions); return true; } catch (global::System.Exception) diff --git a/src/Hume/Core/MultipartFormRequest.cs b/src/Hume/Core/MultipartFormRequest.cs index 92b6359..44f487e 100644 --- a/src/Hume/Core/MultipartFormRequest.cs +++ b/src/Hume/Core/MultipartFormRequest.cs @@ -26,11 +26,7 @@ internal void AddJsonPart(string name, object? value, string? contentType) Utf8NoBom, "application/json" ); - var content = new StringContent( - JsonUtils.SerializeWithAdditionalProperties(value, null), - encoding, - mediaType - ); + var content = new StringContent(JsonUtils.Serialize(value), encoding, mediaType); if (string.IsNullOrEmpty(charset) && content.Headers.ContentType is not null) { content.Headers.ContentType.CharSet = ""; diff --git a/src/Hume/Core/Page.cs b/src/Hume/Core/Page.cs index b797cff..e0284b4 100644 --- a/src/Hume/Core/Page.cs +++ b/src/Hume/Core/Page.cs @@ -1,5 +1,6 @@ using global::System.Collections; using global::System.Collections.ObjectModel; +using global::System.Net; namespace Hume.Core; @@ -19,11 +20,43 @@ public Page(IReadOnlyList items) Items = items; } + /// + /// Creates a new with the specified items and response metadata. + /// + public Page( + IReadOnlyList items, + object? response, + HttpStatusCode statusCode, + ResponseHeaders? headers + ) + { + Items = items; + Response = response; + StatusCode = statusCode; + Headers = headers; + } + /// /// Gets the items in this . /// public IReadOnlyList Items { get; } + /// + /// The full API response object for this page. Cast to the endpoint's response + /// type to access fields beyond the paginated items (e.g., TotalCount, metadata). + /// + public object? Response { get; } + + /// + /// The HTTP status code of the response that produced this page. + /// + public HttpStatusCode StatusCode { get; } + + /// + /// The HTTP response headers from the response that produced this page. + /// + public ResponseHeaders? Headers { get; } + /// /// Enumerate the items in this . /// diff --git a/src/Hume/Core/Pager.cs b/src/Hume/Core/Pager.cs index 2fc2c3f..32f5bbe 100644 --- a/src/Hume/Core/Pager.cs +++ b/src/Hume/Core/Pager.cs @@ -95,7 +95,7 @@ internal sealed class OffsetPager SendRequest( + internal delegate global::System.Threading.Tasks.Task> SendRequest( TRequest request, TRequestOptions? options, CancellationToken cancellationToken @@ -115,7 +115,7 @@ private delegate ( TRequest? nextRequest, bool hasNextPage, Page page - ) ParseApiCallDelegate(TRequest request, TResponse response); + ) ParseApiCallDelegate(TRequest request, WithRawResponse wrappedResponse); public Page CurrentPage { get; private set; } public bool HasNextPage { get; private set; } @@ -152,7 +152,8 @@ bool hasNextPage ) { request ??= Activator.CreateInstance(); - var response = await sendRequest(request, options, cancellationToken).ConfigureAwait(false); + var wrappedResponse = await sendRequest(request, options, cancellationToken) + .ConfigureAwait(false); var parseApiCall = CreateParseApiCallDelegate( getItems, getOffset, @@ -160,7 +161,7 @@ bool hasNextPage getStep, getHasNextPage ); - var (nextRequest, hasNextPage, page) = parseApiCall(request, response); + var (nextRequest, hasNextPage, page) = parseApiCall(request, wrappedResponse); return new OffsetPager( nextRequest, options, @@ -179,10 +180,10 @@ private static ParseApiCallDelegate CreateParseApiCallDelegate( GetHasNextPage? getHasNextPage ) { - return (request, response) => + return (request, wrappedResponse) => ParseApiCall( request, - response, + wrappedResponse, getItems, getOffset, setOffset, @@ -197,9 +198,9 @@ private static ParseApiCallDelegate CreateParseApiCallDelegate( CancellationToken cancellationToken = default ) { - var response = await _sendRequest(request, options, cancellationToken) + var wrappedResponse = await _sendRequest(request, options, cancellationToken) .ConfigureAwait(false); - var (nextRequest, hasNextPageFlag, page) = _parseApiCall(request, response); + var (nextRequest, hasNextPageFlag, page) = _parseApiCall(request, wrappedResponse); _request = nextRequest; HasNextPage = hasNextPageFlag; CurrentPage = page; @@ -208,7 +209,7 @@ private static ParseApiCallDelegate CreateParseApiCallDelegate( private static (TRequest? nextRequest, bool hasNextPage, Page page) ParseApiCall( TRequest request, - TResponse response, + WithRawResponse wrappedResponse, GetItems getItems, GetOffset getOffset, SetOffset setOffset, @@ -216,8 +217,16 @@ private static (TRequest? nextRequest, bool hasNextPage, Page page) Parse GetHasNextPage? getHasNextPage ) { + var response = wrappedResponse.Data; var items = getItems(response); - var page = items is not null ? new Page(items) : Page.Empty; + var page = items is not null + ? new Page( + items, + response, + wrappedResponse.RawResponse.StatusCode, + wrappedResponse.RawResponse.Headers + ) + : Page.Empty; var offset = getOffset(request); var hasNextPage = getHasNextPage?.Invoke(response) ?? items?.Count > 0; if (!hasNextPage) @@ -360,7 +369,7 @@ internal sealed class CursorPager /// Delegate for sending a request. /// - internal delegate global::System.Threading.Tasks.Task SendRequest( + internal delegate global::System.Threading.Tasks.Task> SendRequest( TRequest request, TRequestOptions? options, CancellationToken cancellationToken @@ -385,7 +394,7 @@ private delegate ( TRequest? nextRequest, bool hasNextPage, Page page - ) ParseApiCallDelegate(TRequest request, TResponse response); + ) ParseApiCallDelegate(TRequest request, WithRawResponse wrappedResponse); /// /// The current . @@ -430,9 +439,10 @@ bool hasNextPage ) { request ??= Activator.CreateInstance(); - var response = await sendRequest(request, options, cancellationToken).ConfigureAwait(false); + var wrappedResponse = await sendRequest(request, options, cancellationToken) + .ConfigureAwait(false); var parseApiCall = CreateParseApiCallDelegate(getItems, getNextCursor, setCursor); - var (nextRequest, hasNextPage, page) = parseApiCall(request, response); + var (nextRequest, hasNextPage, page) = parseApiCall(request, wrappedResponse); return new CursorPager( nextRequest, options, @@ -449,10 +459,18 @@ private static ParseApiCallDelegate CreateParseApiCallDelegate( SetCursor setCursor ) { - return (request, response) => + return (request, wrappedResponse) => { + var response = wrappedResponse.Data; var items = getItems(response); - var page = items is not null ? new Page(items) : Page.Empty; + var page = items is not null + ? new Page( + items, + response, + wrappedResponse.RawResponse.StatusCode, + wrappedResponse.RawResponse.Headers + ) + : Page.Empty; var cursor = getNextCursor(response); var hasNextPage = cursor switch { @@ -475,9 +493,9 @@ SetCursor setCursor CancellationToken cancellationToken = default ) { - var response = await _sendRequest(request, options, cancellationToken) + var wrappedResponse = await _sendRequest(request, options, cancellationToken) .ConfigureAwait(false); - var (nextRequest, hasNextPage, page) = _parseApiCall(request, response); + var (nextRequest, hasNextPage, page) = _parseApiCall(request, wrappedResponse); _request = nextRequest; HasNextPage = hasNextPage; CurrentPage = page; diff --git a/src/Hume/Core/Public/ClientOptions.cs b/src/Hume/Core/Public/ClientOptions.cs index 224a5d8..19fbbea 100644 --- a/src/Hume/Core/Public/ClientOptions.cs +++ b/src/Hume/Core/Public/ClientOptions.cs @@ -45,7 +45,7 @@ public partial class ClientOptions } = []; /// - /// The http client used to make requests. + /// The max number of retries to attempt. /// public int MaxRetries { get; #if NET5_0_OR_GREATER diff --git a/src/Hume/Core/Public/RequestOptions.cs b/src/Hume/Core/Public/RequestOptions.cs index a26a6d6..6ee2f62 100644 --- a/src/Hume/Core/Public/RequestOptions.cs +++ b/src/Hume/Core/Public/RequestOptions.cs @@ -40,7 +40,7 @@ public partial class RequestOptions : IRequestOptions } = []; /// - /// The http client used to make requests. + /// The max number of retries to attempt. /// public int? MaxRetries { get; #if NET5_0_OR_GREATER diff --git a/src/Hume/Core/Public/Version.cs b/src/Hume/Core/Public/Version.cs index be85644..afac175 100644 --- a/src/Hume/Core/Public/Version.cs +++ b/src/Hume/Core/Public/Version.cs @@ -3,5 +3,5 @@ namespace Hume; [Serializable] internal class Version { - public const string Current = "0.2.6"; + public const string Current = "0.2.7"; } diff --git a/src/Hume/Core/QueryStringBuilder.cs b/src/Hume/Core/QueryStringBuilder.cs index bed5d49..7611956 100644 --- a/src/Hume/Core/QueryStringBuilder.cs +++ b/src/Hume/Core/QueryStringBuilder.cs @@ -7,18 +7,51 @@ namespace Hume.Core; /// -/// High-performance query string builder with cross-platform optimizations. +/// High-performance query string builder with RFC 3986 compliant percent-encoding. /// Uses span-based APIs on .NET 6+ and StringBuilder fallback for older targets. +/// +/// RFC 3986 defines the following relevant productions: +/// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +/// query = *( pchar / "/" / "?" ) +/// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +/// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" +/// +/// Three encoding contexts are distinguished: +/// Path segment (pchar): unreserved + sub-delims + ":" + "@" +/// Query key: query chars minus "&", "=", "+", "#" +/// Query value: query chars minus "&", "+", "#" /// internal static class QueryStringBuilder { + // ────────────────────────────────────────────────────────────────────── + // RFC 3986 character sets + // + // Query key safe: unreserved + (sub-delims \ {& = +}) + : @ / ? + // Query value safe: unreserved + (sub-delims \ {& +}) + : @ / ? + // Path segment safe: unreserved + sub-delims + : @ + // ────────────────────────────────────────────────────────────────────── + #if NET8_0_OR_GREATER - private static readonly SearchValues UnreservedChars = SearchValues.Create( - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~" + private static readonly SearchValues SafeQueryKeyChars = SearchValues.Create( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,;:@/?" + ); + + private static readonly SearchValues SafeQueryValueChars = SearchValues.Create( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,;=:@/?" + ); + + private static readonly SearchValues SafePathChars = SearchValues.Create( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$&'()*+,;=:@" ); #else - private const string UnreservedChars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"; + private const string SafeQueryKeyChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,;:@/?"; + + private const string SafeQueryValueChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,;=:@/?"; + + private const string SafePathChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$&'()*+,;=:@"; #endif #if NET7_0_OR_GREATER @@ -45,6 +78,43 @@ internal static class QueryStringBuilder }; #endif + private enum EncodingContext + { + QueryKey, + QueryValue, + Path, + } + + /// + /// Percent-encodes a path segment value per RFC 3986 section 3.3 (pchar). + /// Allowed unencoded: unreserved / sub-delims / ":" / "@" + /// + public static string EncodePathSegment(string value) + { + if (string.IsNullOrEmpty(value)) + return value; + +#if NET6_0_OR_GREATER + if (!NeedsEncoding(value.AsSpan(), EncodingContext.Path)) + return value; + + var buffer = ArrayPool.Shared.Rent(value.Length * 3); + try + { + var written = EncodeSlow(value.AsSpan(), buffer.AsSpan(), EncodingContext.Path); + return new string(buffer.AsSpan(0, written)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } +#else + var sb = new StringBuilder(value.Length); + AppendEncoded(sb, value, EncodingContext.Path); + return sb.ToString(); +#endif + } + /// /// Builds a query string from the provided parameters. /// @@ -135,9 +205,17 @@ public static string Build(IEnumerable> parameters) buffer[position++] = first ? '?' : '&'; first = false; - position += EncodeComponent(kvp.Key.AsSpan(), buffer.AsSpan(position)); + position += EncodeWithCharSet( + kvp.Key.AsSpan(), + buffer.AsSpan(position), + EncodingContext.QueryKey + ); buffer[position++] = '='; - position += EncodeComponent(kvp.Value.AsSpan(), buffer.AsSpan(position)); + position += EncodeWithCharSet( + kvp.Value.AsSpan(), + buffer.AsSpan(position), + EncodingContext.QueryValue + ); } while (enumerator.MoveNext()); return first ? string.Empty : new string(buffer.AsSpan(0, position)); @@ -156,9 +234,9 @@ public static string Build(IEnumerable> parameters) sb.Append(first ? '?' : '&'); first = false; - AppendEncoded(sb, kvp.Key); + AppendEncoded(sb, kvp.Key, EncodingContext.QueryKey); sb.Append('='); - AppendEncoded(sb, kvp.Value); + AppendEncoded(sb, kvp.Value, EncodingContext.QueryValue); } return sb.ToString(); @@ -179,48 +257,61 @@ Span buffer buffer[position++] = first ? '?' : '&'; first = false; - position += EncodeComponent(kvp.Key.AsSpan(), buffer.Slice(position)); + position += EncodeWithCharSet( + kvp.Key.AsSpan(), + buffer.Slice(position), + EncodingContext.QueryKey + ); buffer[position++] = '='; - position += EncodeComponent(kvp.Value.AsSpan(), buffer.Slice(position)); + position += EncodeWithCharSet( + kvp.Value.AsSpan(), + buffer.Slice(position), + EncodingContext.QueryValue + ); } return position; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int EncodeComponent(ReadOnlySpan input, Span output) + private static int EncodeWithCharSet( + ReadOnlySpan input, + Span output, + EncodingContext context + ) { - if (!NeedsEncoding(input)) + if (!NeedsEncoding(input, context)) { input.CopyTo(output); return input.Length; } - return EncodeSlow(input, output); + return EncodeSlow(input, output, context); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool NeedsEncoding(ReadOnlySpan value) + private static bool NeedsEncoding(ReadOnlySpan value, EncodingContext context) { -#if NET8_0_OR_GREATER - return value.ContainsAnyExcept(UnreservedChars); -#else - foreach (var c in value) + return context switch { - if (!IsUnreserved(c)) - return true; - } - return false; -#endif + EncodingContext.QueryKey => value.ContainsAnyExcept(SafeQueryKeyChars), + EncodingContext.QueryValue => value.ContainsAnyExcept(SafeQueryValueChars), + EncodingContext.Path => value.ContainsAnyExcept(SafePathChars), + _ => true, + }; } - private static int EncodeSlow(ReadOnlySpan input, Span output) + private static int EncodeSlow( + ReadOnlySpan input, + Span output, + EncodingContext context + ) { var position = 0; foreach (var c in input) { - if (IsUnreserved(c)) + if (IsSafeChar(c, context)) { output[position++] = c; } @@ -230,11 +321,7 @@ private static int EncodeSlow(ReadOnlySpan input, Span output) output[position++] = '2'; output[position++] = '0'; } -#if NET7_0_OR_GREATER else if (char.IsAscii(c)) -#else - else if (c <= 127) -#endif { position += EncodeAscii((byte)c, output.Slice(position)); } @@ -274,11 +361,11 @@ private static int EncodeUtf8(char c, Span output) } #else // netstandard2.0 / net462 StringBuilder-based encoding - private static void AppendEncoded(StringBuilder sb, string value) + private static void AppendEncoded(StringBuilder sb, string value, EncodingContext context) { foreach (var c in value) { - if (IsUnreserved(c)) + if (IsSafeChar(c, context)) { sb.Append(c); } @@ -311,20 +398,103 @@ private static void AppendPercentEncoded(StringBuilder sb, byte value) #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsUnreserved(char c) + private static bool IsSafeChar(char c, EncodingContext context) + { + return context switch + { + EncodingContext.QueryKey => IsSafeQueryKeyChar(c), + EncodingContext.QueryValue => IsSafeQueryValueChar(c), + EncodingContext.Path => IsSafePathChar(c), + _ => false, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSafeQueryKeyChar(char c) + { +#if NET8_0_OR_GREATER + return SafeQueryKeyChars.Contains(c); +#else + // query = *( pchar / "/" / "?" ) minus "&", "=", "+", "#" + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '-' + || c == '_' + || c == '.' + || c == '~' + || c == '!' + || c == '$' + || c == (char)39 // single quote + || c == '(' + || c == ')' + || c == '*' + || c == ',' + || c == ';' + || c == ':' + || c == '@' + || c == '/' + || c == '?'; +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSafeQueryValueChar(char c) + { +#if NET8_0_OR_GREATER + return SafeQueryValueChars.Contains(c); +#else + // query = *( pchar / "/" / "?" ) minus "&", "+", "#" + return (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '-' + || c == '_' + || c == '.' + || c == '~' + || c == '!' + || c == '$' + || c == (char)39 // single quote + || c == '(' + || c == ')' + || c == '*' + || c == ',' + || c == ';' + || c == '=' + || c == ':' + || c == '@' + || c == '/' + || c == '?'; +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSafePathChar(char c) { #if NET8_0_OR_GREATER - return UnreservedChars.Contains(c); -#elif NET7_0_OR_GREATER - return char.IsAsciiLetterOrDigit(c) || c is '-' or '_' or '.' or '~'; + return SafePathChars.Contains(c); #else + // pchar = unreserved / sub-delims / ":" / "@" return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' - || c == '~'; + || c == '~' + || c == '!' + || c == '$' + || c == '&' + || c == (char)39 // single quote + || c == '(' + || c == ')' + || c == '*' + || c == '+' + || c == ',' + || c == ';' + || c == '=' + || c == ':' + || c == '@'; #endif } diff --git a/src/Hume/Core/RawClient.cs b/src/Hume/Core/RawClient.cs index 02bf3d2..057710f 100644 --- a/src/Hume/Core/RawClient.cs +++ b/src/Hume/Core/RawClient.cs @@ -277,6 +277,7 @@ private static bool IsRetryableContent(HttpRequestMessage request) private static string BuildUrl(global::Hume.Core.BaseRequest request) { var baseUrl = request.Options?.BaseUrl ?? request.BaseUrl; + var trimmedBaseUrl = baseUrl.TrimEnd('/'); var trimmedBasePath = request.Path.TrimStart('/'); var url = $"{trimmedBaseUrl}/{trimmedBasePath}"; diff --git a/src/Hume/Core/StringEnum.cs b/src/Hume/Core/StringEnum.cs index 11e5792..af3a27e 100644 --- a/src/Hume/Core/StringEnum.cs +++ b/src/Hume/Core/StringEnum.cs @@ -1,5 +1,3 @@ -using global::System.Text.Json.Serialization; - namespace Hume.Core; public interface IStringEnum : IEquatable diff --git a/src/Hume/Core/StringEnumSerializer.cs b/src/Hume/Core/StringEnumSerializer.cs deleted file mode 100644 index f1b4762..0000000 --- a/src/Hume/Core/StringEnumSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using global::System.Text.Json; -using global::System.Text.Json.Serialization; - -namespace Hume.Core; - -internal class StringEnumSerializer : JsonConverter - where T : IStringEnum -{ - public override T? Read( - ref Utf8JsonReader reader, - global::System.Type typeToConvert, - JsonSerializerOptions options - ) - { - var stringValue = - reader.GetString() - ?? throw new global::System.Exception("The JSON value could not be read as a string."); - return (T?)Activator.CreateInstance(typeToConvert, stringValue); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value); - } -} diff --git a/src/Hume/Core/ValueConvert.cs b/src/Hume/Core/ValueConvert.cs index 56f95f5..556fddb 100644 --- a/src/Hume/Core/ValueConvert.cs +++ b/src/Hume/Core/ValueConvert.cs @@ -29,7 +29,8 @@ public static class ValueConvert internal static string ToPathParameterString(ulong v) => ToString(v); - internal static string ToPathParameterString(string v) => ToString(v); + internal static string ToPathParameterString(string v) => + QueryStringBuilder.EncodePathSegment(v); internal static string ToPathParameterString(char v) => ToString(v); diff --git a/src/Hume/Core/WebSockets/Event.cs b/src/Hume/Core/WebSockets/Event.cs index bc3887c..700665e 100644 --- a/src/Hume/Core/WebSockets/Event.cs +++ b/src/Hume/Core/WebSockets/Event.cs @@ -8,6 +8,7 @@ namespace Hume.Core.WebSockets; /// The type of event data passed to event handlers. public class Event : IDisposable { + private readonly object _lock = new(); private readonly HashSet> _subscribers = []; private readonly HashSet> _subscribersAsync = []; @@ -23,12 +24,20 @@ internal Event() { } /// A task representing the asynchronous operation. internal async global::System.Threading.Tasks.Task RaiseEvent(T eventObject) { - foreach (var subscriber in _subscribers) + Action[] syncSnapshot; + Func[] asyncSnapshot; + lock (_lock) + { + syncSnapshot = [.. _subscribers]; + asyncSnapshot = [.. _subscribersAsync]; + } + + foreach (var subscriber in syncSnapshot) { subscriber.Invoke(eventObject); } - foreach (var subscriber in _subscribersAsync) + foreach (var subscriber in asyncSnapshot) { await subscriber.Invoke(eventObject).ConfigureAwait(false); } @@ -40,7 +49,10 @@ internal Event() { } /// The synchronous event handler to subscribe. public void Subscribe(Action eventHandler) { - _subscribers.Add(eventHandler); + lock (_lock) + { + _subscribers.Add(eventHandler); + } } /// @@ -49,7 +61,10 @@ public void Subscribe(Action eventHandler) /// The asynchronous event handler to subscribe. public void Subscribe(Func eventHandler) { - _subscribersAsync.Add(eventHandler); + lock (_lock) + { + _subscribersAsync.Add(eventHandler); + } } /// @@ -58,7 +73,7 @@ public void Subscribe(Func eventHandler) /// The synchronous event handler to unsubscribe. public void Unsubscribe(Action eventHandler) { - if (_subscribers.Contains(eventHandler)) + lock (_lock) { _subscribers.Remove(eventHandler); } @@ -70,7 +85,7 @@ public void Unsubscribe(Action eventHandler) /// The asynchronous event handler to unsubscribe. public void Unsubscribe(Func eventHandler) { - if (_subscribersAsync.Contains(eventHandler)) + lock (_lock) { _subscribersAsync.Remove(eventHandler); } @@ -81,8 +96,11 @@ public void Unsubscribe(Func eventHandle /// public void UnsubscribeAll() { - _subscribers.Clear(); - _subscribersAsync.Clear(); + lock (_lock) + { + _subscribers.Clear(); + _subscribersAsync.Clear(); + } } /// diff --git a/src/Hume/Core/WebSockets/ReconnectStrategy.cs b/src/Hume/Core/WebSockets/ReconnectStrategy.cs new file mode 100644 index 0000000..390cecd --- /dev/null +++ b/src/Hume/Core/WebSockets/ReconnectStrategy.cs @@ -0,0 +1,90 @@ +// ReSharper disable All +#pragma warning disable +namespace Hume.Core.WebSockets; + +/// +/// Configures the backoff strategy for automatic reconnection. +/// +public class ReconnectStrategy +{ + private readonly Random _jitterRandom = new(); + + private int _attemptsMade; + private TimeSpan _currentInterval; + + /// + /// Gets the number of reconnection attempts made since the last reset. + /// + public int AttemptsMade => _attemptsMade; + + /// + /// Initial delay before the first reconnection attempt. + /// Default: 1 second. + /// + public TimeSpan MinReconnectInterval { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Maximum delay between reconnection attempts. + /// Default: 60 seconds. + /// + public TimeSpan MaxReconnectInterval { get; set; } = TimeSpan.FromSeconds(60); + + /// + /// Maximum number of reconnection attempts before giving up. + /// Set to null for unlimited attempts. Default: null. + /// + public int? MaxAttempts { get; set; } + + /// + /// Add random jitter (0-25% of interval) to prevent thundering herd. + /// Default: true. + /// + public bool UseJitter { get; set; } = true; + + /// + /// Get the next reconnection delay and increment the attempt counter. + /// Returns null if max attempts have been exhausted. + /// + public TimeSpan? GetNextDelay() + { + if (MaxAttempts.HasValue && _attemptsMade >= MaxAttempts.Value) + { + return null; // exhausted + } + + if (_attemptsMade == 0) + { + _currentInterval = MinReconnectInterval; + } + else + { + // Exponential backoff: double the interval, capped at max + _currentInterval = TimeSpan.FromMilliseconds( + Math.Min( + _currentInterval.TotalMilliseconds * 2, + MaxReconnectInterval.TotalMilliseconds + ) + ); + } + + _attemptsMade++; + + if (UseJitter) + { + // Add 0-25% random jitter + var jitterMs = _currentInterval.TotalMilliseconds * 0.25 * _jitterRandom.NextDouble(); + return _currentInterval + TimeSpan.FromMilliseconds(jitterMs); + } + + return _currentInterval; + } + + /// + /// Reset the attempt counter and interval. Call after successful connection. + /// + public void Reset() + { + _attemptsMade = 0; + _currentInterval = MinReconnectInterval; + } +} diff --git a/src/Hume/Core/WebSockets/ReconnectionInfo.cs b/src/Hume/Core/WebSockets/ReconnectionInfo.cs index 091b47b..43cbcb0 100644 --- a/src/Hume/Core/WebSockets/ReconnectionInfo.cs +++ b/src/Hume/Core/WebSockets/ReconnectionInfo.cs @@ -6,7 +6,7 @@ namespace Hume.Core.WebSockets; /// Contains information about a WebSocket reconnection event. /// Provides details about the reason for reconnection. /// -internal class ReconnectionInfo +public class ReconnectionInfo { /// /// Initializes a new instance of the ReconnectionInfo class. diff --git a/src/Hume/Core/WebSockets/ReconnectionType.cs b/src/Hume/Core/WebSockets/ReconnectionType.cs index 8a5057f..56d90b3 100644 --- a/src/Hume/Core/WebSockets/ReconnectionType.cs +++ b/src/Hume/Core/WebSockets/ReconnectionType.cs @@ -5,7 +5,7 @@ namespace Hume.Core.WebSockets; /// /// Specifies the type of reconnection that occurred in a WebSocket connection. /// -internal enum ReconnectionType +public enum ReconnectionType { /// /// Initial connection to the WebSocket stream. diff --git a/src/Hume/Core/WebSockets/WebSocketClient.cs b/src/Hume/Core/WebSockets/WebSocketClient.cs index d882cb6..566dc77 100644 --- a/src/Hume/Core/WebSockets/WebSocketClient.cs +++ b/src/Hume/Core/WebSockets/WebSocketClient.cs @@ -14,6 +14,45 @@ internal sealed class WebSocketClient : IAsyncDisposable, IDisposable, INotifyPr private readonly Uri _uri; private readonly Func _onTextMessage; + /// + /// Enable or disable automatic reconnection. Default: false. + /// + public bool IsReconnectionEnabled { get; set; } + + /// + /// Time to wait before reconnecting if no message comes from the server. + /// Set null to disable. Default: 1 minute. + /// + public TimeSpan? ReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the last reconnection attempt failed. + /// Set null to disable. Default: 1 minute. + /// + public TimeSpan? ErrorReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the connection is lost with a transient error. + /// Set null to disable (reconnect immediately). Default: null. + /// + public TimeSpan? LostReconnectTimeout { get; set; } + + /// + /// How often to check the WebSocket state for silent disconnections. + /// Addresses ReceiveAsync hang when TCP closes without WebSocket notification. + /// See: https://github.com/dotnet/runtime/issues/110496 + /// Set to null to disable. Default: 5 seconds. + /// + public TimeSpan? StateCheckInterval { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Strategy for reconnection backoff delays. + /// Controls interval growth, jitter, and max attempts. + /// Set to null to use fixed-interval reconnection (legacy behavior). + /// Default: exponential backoff, 1s → 60s, unlimited attempts, with jitter. + /// + public ReconnectStrategy? Backoff { get; set; } = new ReconnectStrategy(); + /// /// Initializes a new instance of the WebSocketClient class. /// @@ -25,6 +64,45 @@ public WebSocketClient(Uri uri, Func + /// Optional per-message deflate compression options (RFC 7692). + /// When set, enables WebSocket compression via + /// . + /// Compression is negotiated during the WebSocket handshake; if the server does not + /// support it, the connection proceeds without compression. + /// + /// Security warning: Do not enable compression when transmitting data that + /// contains secrets. Compressed encrypted payloads are vulnerable to CRIME/BREACH + /// side-channel attacks. + /// See + /// ClientWebSocketOptions.DangerousDeflateOptions for details. + /// + /// + public WebSocketDeflateOptions? DeflateOptions { get; set; } +#endif + + /// + /// Optional HttpMessageInvoker for HTTP/2 WebSocket connections. + /// When set, enables multiplexing multiple WebSocket streams over a single TCP connection. + /// Requires .NET 7+. + /// + public System.Net.Http.HttpMessageInvoker? HttpInvoker { get; set; } + + /// + /// Time range for how long to wait while connecting. + /// Default: 5 seconds. + /// + public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(5); + + /// + /// Maximum time to wait for a single SendAsync call to complete. + /// Prevents indefinite hangs when the remote peer dies without closing the connection. + /// See: https://github.com/dotnet/runtime/issues/125257 + /// Default: 30 seconds. + /// + public TimeSpan SendTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// /// Gets the current connection status of the WebSocket. /// @@ -58,10 +136,13 @@ private void EnsureConnected() /// The text message to send. /// A task representing the asynchronous send operation. /// Thrown when the connection is not in Connected status. - public global::System.Threading.Tasks.Task SendInstant(string message) + public global::System.Threading.Tasks.Task SendInstant( + string message, + CancellationToken cancellationToken = default + ) { EnsureConnected(); - return _webSocket!.SendInstant(message); + return _webSocket!.SendInstant(message, cancellationToken); } /// @@ -70,10 +151,13 @@ private void EnsureConnected() /// The binary message to send as a Memory<byte>. /// A task representing the asynchronous send operation. /// Thrown when the connection is not in Connected status. - public global::System.Threading.Tasks.Task SendInstant(Memory message) + public global::System.Threading.Tasks.Task SendInstant( + Memory message, + CancellationToken cancellationToken = default + ) { EnsureConnected(); - return _webSocket!.SendInstant(message); + return _webSocket!.SendInstant(message, cancellationToken); } /// @@ -82,10 +166,13 @@ private void EnsureConnected() /// The binary message to send as an ArraySegment<byte>. /// A task representing the asynchronous send operation. /// Thrown when the connection is not in Connected status. - public global::System.Threading.Tasks.Task SendInstant(ArraySegment message) + public global::System.Threading.Tasks.Task SendInstant( + ArraySegment message, + CancellationToken cancellationToken = default + ) { EnsureConnected(); - return _webSocket!.SendInstant(message); + return _webSocket!.SendInstant(message, cancellationToken); } /// @@ -94,10 +181,31 @@ private void EnsureConnected() /// The binary message to send as a byte array. /// A task representing the asynchronous send operation. /// Thrown when the connection is not in Connected status. - public global::System.Threading.Tasks.Task SendInstant(byte[] message) + public global::System.Threading.Tasks.Task SendInstant( + byte[] message, + CancellationToken cancellationToken = default + ) + { + EnsureConnected(); + return _webSocket!.SendInstant(message, cancellationToken); + } + + /// + /// Queues a text message for sending on a background thread. Non-blocking. + /// + public bool Send(string message) { EnsureConnected(); - return _webSocket!.SendInstant(message); + return _webSocket!.Send(message); + } + + /// + /// Queues a binary message for sending on a background thread. Non-blocking. + /// + public bool Send(byte[] message) + { + EnsureConnected(); + return _webSocket!.Send(message); } /// @@ -148,18 +256,21 @@ private void DisposeEventsInternal() ExceptionOccurred.Dispose(); Closed.Dispose(); Connected.Dispose(); + Reconnecting.Dispose(); } /// /// Asynchronously closes the WebSocket connection with normal closure status. /// /// A task representing the asynchronous close operation. - public async global::System.Threading.Tasks.Task CloseAsync() + public async global::System.Threading.Tasks.Task CloseAsync( + CancellationToken cancellationToken = default + ) { if (_webSocket is not null) { Status = ConnectionStatus.Disconnecting; - await _webSocket.StopOrFail(WebSocketCloseStatus.NormalClosure, ""); + await _webSocket.StopOrFail(WebSocketCloseStatus.NormalClosure, "", cancellationToken); Status = ConnectionStatus.Disconnected; } } @@ -169,7 +280,9 @@ private void DisposeEventsInternal() /// /// A task representing the asynchronous connect operation. /// Thrown when the connection status is not Disconnected or when connection fails. - public async global::System.Threading.Tasks.Task ConnectAsync() + public async global::System.Threading.Tasks.Task ConnectAsync( + CancellationToken cancellationToken = default + ) { this.Assert( Status == ConnectionStatus.Disconnected, @@ -180,8 +293,20 @@ private void DisposeEventsInternal() Status = ConnectionStatus.Connecting; - _webSocket = new WebSocketConnection(_uri, () => new ClientWebSocket()) + _webSocket = new WebSocketConnection(_uri) { +#if NET6_0_OR_GREATER + DeflateOptions = DeflateOptions, +#endif + HttpInvoker = HttpInvoker, + ConnectTimeout = ConnectTimeout, + SendTimeout = SendTimeout, + IsReconnectionEnabled = IsReconnectionEnabled, + ReconnectTimeout = ReconnectTimeout, + ErrorReconnectTimeout = ErrorReconnectTimeout, + LostReconnectTimeout = LostReconnectTimeout, + StateCheckInterval = StateCheckInterval, + Backoff = Backoff, ExceptionOccurred = ExceptionOccurred.RaiseEvent, TextMessageReceived = _onTextMessage, BinaryMessageReceived = stream => @@ -191,17 +316,23 @@ private void DisposeEventsInternal() }, DisconnectionHappened = async d => { + Status = ConnectionStatus.Disconnected; await Closed .RaiseEvent( new Closed { Code = (int?)d.CloseStatus, Reason = d.CloseStatusDescription } ) .ConfigureAwait(false); }, + ReconnectionHappened = async info => + { + Status = ConnectionStatus.Connected; + await Reconnecting.RaiseEvent(info).ConfigureAwait(false); + }, }; try { - await _webSocket.StartOrFail().ConfigureAwait(false); + await _webSocket.StartOrFail(cancellationToken).ConfigureAwait(false); Status = ConnectionStatus.Connected; await Connected.RaiseEvent(new Connected()).ConfigureAwait(false); } @@ -227,6 +358,11 @@ await Closed /// public readonly Event ExceptionOccurred = new(); + /// + /// Event that is raised when the WebSocket connection is re-established after a disconnect. + /// + public readonly Event Reconnecting = new(); + /// /// Event that is raised when a property value changes. /// Currently only raised for the Status property. diff --git a/src/Hume/Core/WebSockets/WebSocketConnection.Sending.cs b/src/Hume/Core/WebSockets/WebSocketConnection.Sending.cs index 6afc83c..33ec360 100644 --- a/src/Hume/Core/WebSockets/WebSocketConnection.Sending.cs +++ b/src/Hume/Core/WebSockets/WebSocketConnection.Sending.cs @@ -2,11 +2,72 @@ #pragma warning disable using global::System.Net.WebSockets; using global::System.Text; +using global::System.Threading.Channels; +using Microsoft.Extensions.Logging; namespace Hume.Core.WebSockets; internal partial class WebSocketConnection { + // Timestamped wrapper for queue messages + private readonly record struct QueuedMessage(DateTime EnqueuedAt, T Payload); + + private Channel>? _textSendQueue; + private Channel>? _binarySendQueue; + + private void InitializeSendQueues() + { + if (SendQueueLimit > 0) + { + _textSendQueue = Channel.CreateBounded>( + new BoundedChannelOptions(SendQueueLimit) + { + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.DropWrite, + } + ); + _binarySendQueue = Channel.CreateBounded>( + new BoundedChannelOptions(SendQueueLimit) + { + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.DropWrite, + } + ); + } + else + { + _textSendQueue = Channel.CreateUnbounded>( + new UnboundedChannelOptions { SingleReader = true, SingleWriter = false } + ); + _binarySendQueue = Channel.CreateUnbounded>( + new UnboundedChannelOptions { SingleReader = true, SingleWriter = false } + ); + } + } + + /// + /// Queues a text message for sending. Actual send happens on a background thread. + /// + /// true if the message was written to the queue + public bool Send(string message) + { + return _textSendQueue?.Writer.TryWrite(new QueuedMessage(DateTime.UtcNow, message)) + ?? false; + } + + /// + /// Queues a binary message for sending. Actual send happens on a background thread. + /// + /// true if the message was written to the queue + public bool Send(byte[] message) + { + return _binarySendQueue?.Writer.TryWrite( + new QueuedMessage(DateTime.UtcNow, message) + ) ?? false; + } + /// /// Send text message to the websocket channel. /// It doesn't use a sending queue, @@ -14,9 +75,12 @@ internal partial class WebSocketConnection /// on the full .NET Framework platform /// /// Message to be sent - public global::System.Threading.Tasks.Task SendInstant(string message) + public global::System.Threading.Tasks.Task SendInstant( + string message, + CancellationToken cancellationToken = default + ) { - return SendInternalSynchronized(new RequestTextMessage(message)); + return SendInternalSynchronized(new RequestTextMessage(message), cancellationToken); } /// @@ -26,9 +90,12 @@ internal partial class WebSocketConnection /// on the full .NET Framework platform /// /// Message to be sent - public global::System.Threading.Tasks.Task SendInstant(Memory message) + public global::System.Threading.Tasks.Task SendInstant( + Memory message, + CancellationToken cancellationToken = default + ) { - return SendInternalSynchronized(message); + return SendInternalSynchronized(message, cancellationToken); } /// @@ -38,9 +105,12 @@ internal partial class WebSocketConnection /// on the full .NET Framework platform /// /// Message to be sent - public global::System.Threading.Tasks.Task SendInstant(ArraySegment message) + public global::System.Threading.Tasks.Task SendInstant( + ArraySegment message, + CancellationToken cancellationToken = default + ) { - return SendInternalSynchronized(message); + return SendInternalSynchronized(message, cancellationToken); } /// @@ -50,22 +120,29 @@ internal partial class WebSocketConnection /// on the full .NET Framework platform /// /// Message to be sent - public global::System.Threading.Tasks.Task SendInstant(byte[] message) + public global::System.Threading.Tasks.Task SendInstant( + byte[] message, + CancellationToken cancellationToken = default + ) { - return SendInternalSynchronized(new ArraySegment(message)); + return SendInternalSynchronized(new ArraySegment(message), cancellationToken); } private async global::System.Threading.Tasks.Task SendInternalSynchronized( - RequestMessage message + RequestMessage message, + CancellationToken cancellationToken = default ) { using (await _locker.LockAsync()) { - await SendInternal(message); + await SendInternal(message, cancellationToken); } } - private async global::System.Threading.Tasks.Task SendInternal(RequestMessage message) + private async global::System.Threading.Tasks.Task SendInternal( + RequestMessage message, + CancellationToken cancellationToken = default + ) { if (!IsClientConnected()) { @@ -73,84 +150,257 @@ RequestMessage message } ArraySegment payload; + WebSocketMessageType messageType; switch (message) { case RequestTextMessage textMessage: payload = new ArraySegment(Encoding.UTF8.GetBytes(textMessage.Text)); + messageType = WebSocketMessageType.Text; break; case RequestBinaryMessage binaryMessage: payload = new ArraySegment(binaryMessage.Data); + messageType = WebSocketMessageType.Binary; break; case RequestBinarySegmentMessage segmentMessage: payload = segmentMessage.Data; + messageType = WebSocketMessageType.Binary; break; default: throw new ArgumentException($"Unknown message type: {message.GetType()}"); } - await _client - .SendAsync( - payload, - WebSocketMessageType.Text, - true, - _cancellation?.Token ?? CancellationToken.None + using var sendCts = + cancellationToken != default + ? CancellationTokenSource.CreateLinkedTokenSource( + _cancellation?.Token ?? CancellationToken.None, + cancellationToken + ) + : CancellationTokenSource.CreateLinkedTokenSource( + _cancellation?.Token ?? CancellationToken.None + ); + sendCts.CancelAfter(SendTimeout); + + try + { + await _client + .SendAsync(payload, messageType, true, sendCts.Token) + .ConfigureAwait(false); + } + catch (OperationCanceledException) + when (sendCts.IsCancellationRequested + && !cancellationToken.IsCancellationRequested + && !(_cancellation?.IsCancellationRequested ?? false) ) - .ConfigureAwait(false); + { + _logger.LogWarning( + "SendAsync timed out after {Timeout}ms, aborting connection", + SendTimeout.TotalMilliseconds + ); + _client?.Abort(); + throw new WebsocketException( + $"SendAsync timed out after {SendTimeout.TotalMilliseconds}ms. " + + "The remote peer may be unreachable. Connection has been aborted." + ); + } } private async global::System.Threading.Tasks.Task SendInternalSynchronized( - ArraySegment message + ArraySegment message, + CancellationToken cancellationToken = default ) { using (await _locker.LockAsync()) { - await SendInternal(message); + await SendInternal(message, cancellationToken); } } - private async global::System.Threading.Tasks.Task SendInternalSynchronized(Memory message) + private async global::System.Threading.Tasks.Task SendInternalSynchronized( + Memory message, + CancellationToken cancellationToken = default + ) { using (await _locker.LockAsync()) { - await SendInternal(message); + await SendInternal(message, cancellationToken); } } - private async global::System.Threading.Tasks.Task SendInternal(ArraySegment payload) + private async global::System.Threading.Tasks.Task SendInternal( + ArraySegment payload, + CancellationToken cancellationToken = default + ) { if (!IsClientConnected()) { return; } - await _client - .SendAsync( - payload, - WebSocketMessageType.Binary, - true, - _cancellation?.Token ?? CancellationToken.None + using var sendCts = + cancellationToken != default + ? CancellationTokenSource.CreateLinkedTokenSource( + _cancellation?.Token ?? CancellationToken.None, + cancellationToken + ) + : CancellationTokenSource.CreateLinkedTokenSource( + _cancellation?.Token ?? CancellationToken.None + ); + sendCts.CancelAfter(SendTimeout); + + try + { + await _client + .SendAsync(payload, WebSocketMessageType.Binary, true, sendCts.Token) + .ConfigureAwait(false); + } + catch (OperationCanceledException) + when (sendCts.IsCancellationRequested + && !cancellationToken.IsCancellationRequested + && !(_cancellation?.IsCancellationRequested ?? false) ) - .ConfigureAwait(false); + { + _logger.LogWarning( + "SendAsync timed out after {Timeout}ms, aborting connection", + SendTimeout.TotalMilliseconds + ); + _client?.Abort(); + throw new WebsocketException( + $"SendAsync timed out after {SendTimeout.TotalMilliseconds}ms. " + + "The remote peer may be unreachable. Connection has been aborted." + ); + } } - private async global::System.Threading.Tasks.Task SendInternal(ReadOnlyMemory payload) + private async global::System.Threading.Tasks.Task SendInternal( + ReadOnlyMemory payload, + CancellationToken cancellationToken = default + ) { if (!IsClientConnected()) { return; } + + using var sendCts = + cancellationToken != default + ? CancellationTokenSource.CreateLinkedTokenSource( + _cancellation?.Token ?? CancellationToken.None, + cancellationToken + ) + : CancellationTokenSource.CreateLinkedTokenSource( + _cancellation?.Token ?? CancellationToken.None + ); + sendCts.CancelAfter(SendTimeout); + + try + { #if NET6_0_OR_GREATER - await _client - .SendAsync( - payload, - WebSocketMessageType.Binary, - true, - _cancellation?.Token ?? CancellationToken.None - ) - .ConfigureAwait(false); + await _client + .SendAsync(payload, WebSocketMessageType.Binary, true, sendCts.Token) + .ConfigureAwait(false); #else - await SendInternal(new ArraySegment(payload.ToArray())).ConfigureAwait(false); + await _client + .SendAsync( + new ArraySegment(payload.ToArray()), + WebSocketMessageType.Binary, + true, + sendCts.Token + ) + .ConfigureAwait(false); #endif + } + catch (OperationCanceledException) + when (sendCts.IsCancellationRequested + && !cancellationToken.IsCancellationRequested + && !(_cancellation?.IsCancellationRequested ?? false) + ) + { + _logger.LogWarning( + "SendAsync timed out after {Timeout}ms, aborting connection", + SendTimeout.TotalMilliseconds + ); + _client?.Abort(); + throw new WebsocketException( + $"SendAsync timed out after {SendTimeout.TotalMilliseconds}ms. " + + "The remote peer may be unreachable. Connection has been aborted." + ); + } + } + + private async global::System.Threading.Tasks.Task DrainTextQueue(CancellationToken token) + { + if (_textSendQueue == null) + return; + try + { + while (await _textSendQueue.Reader.WaitToReadAsync(token)) + { + while (_textSendQueue.Reader.TryRead(out var msg)) + { + // Skip expired messages + if ( + SendCacheItemTimeout.HasValue + && msg.EnqueuedAt.Add(SendCacheItemTimeout.Value) < DateTime.UtcNow + ) + { + _logger.LogDebug( + "Dropping expired text message (enqueued {EnqueuedAt})", + msg.EnqueuedAt + ); + continue; + } + + try + { + await SendInternalSynchronized(new RequestTextMessage(msg.Payload)); + } + catch (OperationCanceledException) { } + catch (Exception e) + { + await OnExceptionOccurred(e).ConfigureAwait(false); + } + } + } + } + catch (OperationCanceledException) { } + } + + private async global::System.Threading.Tasks.Task DrainBinaryQueue(CancellationToken token) + { + if (_binarySendQueue == null) + return; + try + { + while (await _binarySendQueue.Reader.WaitToReadAsync(token)) + { + while (_binarySendQueue.Reader.TryRead(out var msg)) + { + // Skip expired messages + if ( + SendCacheItemTimeout.HasValue + && msg.EnqueuedAt.Add(SendCacheItemTimeout.Value) < DateTime.UtcNow + ) + { + _logger.LogDebug( + "Dropping expired binary message (enqueued {EnqueuedAt})", + msg.EnqueuedAt + ); + continue; + } + + try + { + await SendInternalSynchronized(new ArraySegment(msg.Payload)); + } + catch (OperationCanceledException) { } + catch (Exception e) + { + await OnExceptionOccurred(e).ConfigureAwait(false); + } + } + } + } + catch (OperationCanceledException) { } } } diff --git a/src/Hume/Core/WebSockets/WebSocketConnection.cs b/src/Hume/Core/WebSockets/WebSocketConnection.cs index b2da618..aeecd17 100644 --- a/src/Hume/Core/WebSockets/WebSocketConnection.cs +++ b/src/Hume/Core/WebSockets/WebSocketConnection.cs @@ -28,34 +28,48 @@ private readonly Func< private Timer _errorReconnectTimer; + private DateTime _lastReceivedMsg = DateTime.UtcNow; + private bool _disposing; private bool _reconnecting; private bool _stopping; - private bool _isReconnectionEnabled = true; + private bool _isReconnectionEnabled; + + /// + /// Enable or disable automatic reconnection. Default: false. + /// + public bool IsReconnectionEnabled + { + get => _isReconnectionEnabled; + set => _isReconnectionEnabled = value; + } + private WebSocket _client; private CancellationTokenSource _cancellation; + private CancellationTokenSource _cancellationConnection; private CancellationTokenSource _cancellationTotal; + private readonly Func? _clientFactory; /// /// A simple websocket client with built-in reconnection and error handling /// /// Target websocket url (wss://) - /// Optional factory for native ClientWebSocket, use it whenever you need some custom features (proxy, settings, etc) + /// Optional factory for native ClientWebSocket, use it whenever you need some custom features (proxy, settings, etc). Note: when providing a factory, you are responsible for configuring keep-alive on the returned ClientWebSocket instance. public WebSocketConnection(Uri url, Func? clientFactory = null) - : this(url, null, GetClientFactory(clientFactory)) { } + : this(url, null, null, clientFactory) { } /// /// A simple websocket client with built-in reconnection and error handling /// /// Target websocket url (wss://) /// Logger instance, can be null - /// Optional factory for native ClientWebSocket, use it whenever you need some custom features (proxy, settings, etc) + /// Optional factory for native ClientWebSocket, use it whenever you need some custom features (proxy, settings, etc). Note: when providing a factory, you are responsible for configuring keep-alive on the returned ClientWebSocket instance. public WebSocketConnection( Uri url, ILogger logger, Func? clientFactory = null ) - : this(url, logger, GetClientFactory(clientFactory)) { } + : this(url, logger, null, clientFactory) { } /// /// A simple websocket client with built-in reconnection and error handling @@ -71,21 +85,57 @@ public WebSocketConnection( CancellationToken, global::System.Threading.Tasks.Task > connectionFactory + ) + : this(url, logger, connectionFactory, null) { } + + private WebSocketConnection( + Uri url, + ILogger logger, + Func< + Uri, + CancellationToken, + global::System.Threading.Tasks.Task + > connectionFactory, + Func? clientFactory ) { _logger = logger ?? NullLogger.Instance; Url = url; + _clientFactory = clientFactory; _connectionFactory = connectionFactory ?? ( async (uri, token) => { - //var client = new ClientWebSocket - //{ - // Options = { KeepAliveInterval = new TimeSpan(0, 0, 5, 0) } - //}; - var client = new ClientWebSocket(); - await client.ConnectAsync(uri, token).ConfigureAwait(false); + var client = _clientFactory != null ? _clientFactory() : new ClientWebSocket(); + client.Options.KeepAliveInterval = KeepAliveInterval; +#if NET6_0_OR_GREATER + if (DeflateOptions != null) + { + client.Options.DangerousDeflateOptions = DeflateOptions; + } +#endif +#if NET9_0_OR_GREATER + client.Options.KeepAliveTimeout = KeepAliveTimeout; +#endif + if (HttpInvoker != null) + { +#if NET7_0_OR_GREATER + client.Options.HttpVersion = System.Net.HttpVersion.Version20; + client.Options.HttpVersionPolicy = System + .Net + .Http + .HttpVersionPolicy + .RequestVersionOrHigher; + await client.ConnectAsync(uri, HttpInvoker, token).ConfigureAwait(false); +#else + await client.ConnectAsync(uri, token).ConfigureAwait(false); +#endif + } + else + { + await client.ConnectAsync(uri, token).ConfigureAwait(false); + } return client; } ); @@ -93,6 +143,86 @@ public WebSocketConnection( public Uri Url { get; set; } + /// + /// Optional HttpMessageInvoker for HTTP/2 WebSocket connections. + /// When set, enables multiplexing multiple WebSocket streams over a single TCP connection. + /// Requires .NET 7+. + /// + public System.Net.Http.HttpMessageInvoker? HttpInvoker { get; set; } + + /// + /// Interval for sending keep-alive frames. Default: 30 seconds. + /// Set to TimeSpan.Zero to disable keep-alive. + /// + public TimeSpan KeepAliveInterval { get; set; } = WebSocket.DefaultKeepAliveInterval; + + /// + /// Timeout for expecting a PONG response to a PING keep-alive frame (.NET 9+). + /// Set to a positive finite TimeSpan to enable PING/PONG keep-alive strategy. + /// Default: 20 seconds. + /// + public TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(20); + + /// + /// Time range for how long to wait before reconnecting if no message comes from server. + /// Set null to disable. Default: 1 minute. + /// + public TimeSpan? ReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time range for how long to wait before reconnecting if last reconnection failed. + /// Set null to disable. Default: 1 minute. + /// + public TimeSpan? ErrorReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time range for how long to wait before reconnecting if connection is lost with a transient error. + /// Set null to disable. Default: null/disabled (reconnect immediately). + /// + public TimeSpan? LostReconnectTimeout { get; set; } + + /// + /// Strategy for reconnection backoff delays. + /// Controls interval growth, jitter, and max attempts. + /// Set to null to use fixed-interval reconnection (legacy behavior). + /// Default: exponential backoff, 1s → 60s, unlimited attempts, with jitter. + /// + public ReconnectStrategy? Backoff { get; set; } = new ReconnectStrategy(); + + /// + /// Maximum time to wait for a single SendAsync call to complete. + /// Prevents indefinite hangs when the remote peer dies without closing the connection. + /// See: https://github.com/dotnet/runtime/issues/125257 + /// Default: 30 seconds. + /// + public TimeSpan SendTimeout { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// How often to check the WebSocket state for silent disconnections. + /// Addresses ReceiveAsync hang when TCP closes without WebSocket notification. + /// See: https://github.com/dotnet/runtime/issues/110496 + /// Set to null to disable. Default: 5 seconds. + /// + public TimeSpan? StateCheckInterval { get; set; } = TimeSpan.FromSeconds(5); + +#if NET6_0_OR_GREATER + /// + /// Optional per-message deflate compression options (RFC 7692). + /// When set, the created by the default connection factory + /// assigns this value to . + /// Compression is negotiated during the WebSocket handshake; if the server does not + /// support it, the connection proceeds without compression. + /// + /// Security warning: Do not enable compression when transmitting data that + /// contains secrets. Compressed encrypted payloads are vulnerable to CRIME/BREACH + /// side-channel attacks. + /// See + /// ClientWebSocketOptions.DangerousDeflateOptions for details. + /// + /// + public WebSocketDeflateOptions? DeflateOptions { get; set; } +#endif + public Func? TextMessageReceived { get; set; } public Func? BinaryMessageReceived { get; set; } public Func< @@ -100,6 +230,10 @@ public Func< global::System.Threading.Tasks.Task >? DisconnectionHappened { get; set; } public Func? ExceptionOccurred { get; set; } + public Func< + ReconnectionInfo, + global::System.Threading.Tasks.Task + >? ReconnectionHappened { get; set; } private global::System.Threading.Tasks.Task OnTextMessageReceived(Stream stream) => TextMessageReceived is null @@ -121,6 +255,11 @@ ExceptionOccurred is null ? global::System.Threading.Tasks.Task.CompletedTask : ExceptionOccurred.Invoke(exception); + private global::System.Threading.Tasks.Task OnReconnectionHappened(ReconnectionInfo info) => + ReconnectionHappened is null + ? global::System.Threading.Tasks.Task.CompletedTask + : ReconnectionHappened.Invoke(info); + /// /// Get or set the name of the current websocket client instance. /// For logging purpose (in case you use more parallel websocket clients and want to distinguish between them) @@ -137,6 +276,20 @@ ExceptionOccurred is null /// public bool IsRunning { get; private set; } + /// + /// Maximum number of messages allowed in the send queue. + /// When the queue is full, new messages are silently dropped (DropWrite). + /// Default: 10,000. Set to 0 for unbounded. + /// + public int SendQueueLimit { get; set; } = 10_000; + + /// + /// Maximum age for messages in the send queue. + /// Messages older than this are silently dropped during drain. + /// Default: 30 minutes. Set to null to disable expiration. + /// + public TimeSpan? SendCacheItemTimeout { get; set; } = TimeSpan.FromMinutes(30); + /// /// Enable or disable text message conversion from binary to string (via 'MessageEncoding' property). /// Default: true @@ -152,6 +305,12 @@ ExceptionOccurred is null /// public bool IsStreamDisposedAutomatically { get; set; } = true; + /// + /// Time range for how long to wait while connecting. + /// Default: 5 seconds. + /// + public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(5); + public Encoding MessageEncoding { get; set; } public ClientWebSocket NativeClient => GetSpecificOrThrow(_client); @@ -166,10 +325,14 @@ public void Dispose() { _lastChanceTimer?.Dispose(); _errorReconnectTimer?.Dispose(); + _textSendQueue?.Writer.TryComplete(); + _binarySendQueue?.Writer.TryComplete(); + _cancellationConnection?.Cancel(); _cancellation?.Cancel(); - _cancellationTotal?.Cancel(); _client?.Abort(); + _cancellationTotal?.Cancel(); _client?.Dispose(); + _cancellationConnection?.Dispose(); _cancellation?.Dispose(); _cancellationTotal?.Dispose(); } @@ -204,9 +367,11 @@ public void Dispose() /// In case of connection error it throws an exception. /// Fail fast approach. /// - public global::System.Threading.Tasks.Task StartOrFail() + public global::System.Threading.Tasks.Task StartOrFail( + CancellationToken cancellationToken = default + ) { - return StartInternal(true); + return StartInternal(true, cancellationToken); } /// @@ -232,35 +397,27 @@ string statusDescription /// Returns true if close was initiated successfully public async global::System.Threading.Tasks.Task StopOrFail( WebSocketCloseStatus status, - string statusDescription + string statusDescription, + CancellationToken cancellationToken = default ) { - var result = await StopInternal(_client, status, statusDescription, null, true, false) + var result = await StopInternal( + _client, + status, + statusDescription, + cancellationToken, + true, + false + ) .ConfigureAwait(false); OnDisconnectionHappened(DisconnectionInfo.Create(DisconnectionType.ByUser, _client, null)); return result; } - private static Func< - Uri, - CancellationToken, - global::System.Threading.Tasks.Task - > GetClientFactory(Func clientFactory) - { - if (clientFactory is null) - return null; - - return ( - async (uri, token) => - { - var client = clientFactory(); - await client.ConnectAsync(uri, token).ConfigureAwait(false); - return client; - } - ); - } - - private async global::System.Threading.Tasks.Task StartInternal(bool failFast) + private async global::System.Threading.Tasks.Task StartInternal( + bool failFast, + CancellationToken cancellationToken = default + ) { if (_disposing) { @@ -276,10 +433,21 @@ private static Func< IsStarted = true; - _cancellation = new CancellationTokenSource(); + _cancellation = + cancellationToken != default + ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) + : new CancellationTokenSource(); _cancellationTotal = new CancellationTokenSource(); - await StartClient(Url, _cancellation.Token).ConfigureAwait(false); + InitializeSendQueues(); + + await StartClient(Url, _cancellation.Token, failFast).ConfigureAwait(false); + + _ = global::System.Threading.Tasks.Task.Run(() => DrainTextQueue(_cancellationTotal.Token)); + + _ = global::System.Threading.Tasks.Task.Run(() => + DrainBinaryQueue(_cancellationTotal.Token) + ); } private async global::System.Threading.Tasks.Task StopInternal( @@ -337,12 +505,61 @@ bool byServer return result; } - private async global::System.Threading.Tasks.Task StartClient(Uri uri, CancellationToken token) + private async global::System.Threading.Tasks.Task StartClient( + Uri uri, + CancellationToken token, + bool failFast = false + ) { - _client = await _connectionFactory(uri, token).ConfigureAwait(false); - _ = Listen(_client, token); - IsRunning = true; - IsStarted = true; + DeactivateLastChance(); + try + { + _cancellationConnection = CancellationTokenSource.CreateLinkedTokenSource(token); + _cancellationConnection.CancelAfter(ConnectTimeout); + _client = await _connectionFactory(uri, _cancellationConnection.Token) + .ConfigureAwait(false); + _ = Listen(_client, token); + IsRunning = true; + IsStarted = true; + _lastReceivedMsg = DateTime.UtcNow; + ActivateLastChance(); + } + catch (Exception e) + { + IsRunning = _client?.State == WebSocketState.Open; + var info = DisconnectionInfo.Create(DisconnectionType.Error, _client, e); + await OnDisconnectionHappened(info).ConfigureAwait(false); + + if (failFast) + throw; + + if (info.CancelReconnection) + return; + + if (Backoff != null) + { + // Let the backoff strategy control retry timing + _ = ReconnectSynchronized(ReconnectionType.Error, false, e); + return; + } + + if (ErrorReconnectTimeout == null) + return; + + var timeout = ErrorReconnectTimeout.Value; + _errorReconnectTimer?.Dispose(); + _errorReconnectTimer = new Timer( + _ => + { + if (_client != null && ShouldIgnoreReconnection(_client)) + return; + _ = ReconnectSynchronized(ReconnectionType.Error, false, e); + }, + null, + timeout, + Timeout.InfiniteTimeSpan + ); + } } private bool IsClientConnected() @@ -352,11 +569,58 @@ private bool IsClientConnected() internal Exception ListenException { get; set; } + private async global::System.Threading.Tasks.Task MonitorState( + WebSocket client, + CancellationTokenSource receiveCts + ) + { + if (StateCheckInterval == null) + return; + + try + { + while (!receiveCts.Token.IsCancellationRequested) + { + await global::System + .Threading.Tasks.Task.Delay(StateCheckInterval.Value, receiveCts.Token) + .ConfigureAwait(false); + + if ( + client.State == WebSocketState.Closed + || client.State == WebSocketState.Aborted + || client.State == WebSocketState.CloseReceived + ) + { + _logger.LogWarning( + "State monitor detected WebSocket in '{State}' state, " + + "cancelling receive loop", + client.State + ); + + // Cancel the ReceiveAsync that may be hung + receiveCts.Cancel(); + return; + } + } + } + catch (OperationCanceledException) { } + catch (ObjectDisposedException) { } + } + private async global::System.Threading.Tasks.Task Listen( WebSocket client, CancellationToken token ) { + // Create a linked CTS that the state monitor can cancel + using var receiveCts = CancellationTokenSource.CreateLinkedTokenSource(token); + + // Start the state monitor as a background task + var monitorTask = MonitorState(client, receiveCts); + + Exception causedException = null; + var closedByServer = false; + var cancelReconnect = false; try { // define buffer here and reuse, to avoid more allocation @@ -370,7 +634,7 @@ CancellationToken token WebSocketReceiveResult result; while (true) { - result = await client.ReceiveAsync(buffer, token); + result = await client.ReceiveAsync(buffer, receiveCts.Token); ms.Write(buffer.AsSpan(0, result.Count)); if (result.EndOfMessage) @@ -382,9 +646,11 @@ CancellationToken token switch (result.MessageType) { case WebSocketMessageType.Text: + _lastReceivedMsg = DateTime.UtcNow; await OnTextMessageReceived(ms).ConfigureAwait(false); break; case WebSocketMessageType.Binary: + _lastReceivedMsg = DateTime.UtcNow; await OnBinaryMessageReceived(ms).ConfigureAwait(false); break; case WebSocketMessageType.Close: @@ -405,21 +671,25 @@ await StopInternal( true ); - return; + closedByServer = true; + cancelReconnect = info.CancelReconnection; + break; } } } - } while (client.State == WebSocketState.Open && !token.IsCancellationRequested); + } while ( + client.State == WebSocketState.Open && !receiveCts.Token.IsCancellationRequested + ); } - catch (TaskCanceledException e) + catch (TaskCanceledException) { // task was canceled, ignore } - catch (OperationCanceledException e) + catch (OperationCanceledException) { // operation was canceled, ignore } - catch (ObjectDisposedException e) + catch (ObjectDisposedException) { // client was disposed, ignore } @@ -431,6 +701,225 @@ await StopInternal( e.Message ); await OnExceptionOccurred(e).ConfigureAwait(false); + causedException = e; + } + finally + { + // Ensure monitor task completes + receiveCts.Cancel(); + try + { + await monitorTask.ConfigureAwait(false); + } + catch { } + } + + if (ShouldIgnoreReconnection(client) || !IsStarted) + { + return; + } + + if (LostReconnectTimeout.HasValue) + { + try + { + await global::System + .Threading.Tasks.Task.Delay(LostReconnectTimeout.Value, token) + .ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + } + + if (!cancelReconnect) + { + _ = ReconnectSynchronized( + closedByServer ? ReconnectionType.ByServer : ReconnectionType.Lost, + false, + causedException + ); + } + } + + public global::System.Threading.Tasks.Task Reconnect() => ReconnectInternal(false); + + public global::System.Threading.Tasks.Task ReconnectOrFail() => ReconnectInternal(true); + + private async global::System.Threading.Tasks.Task ReconnectInternal(bool failFast) + { + if (!IsStarted) + return; + try + { + await ReconnectSynchronized(ReconnectionType.ByUser, failFast, null) + .ConfigureAwait(false); + } + finally + { + _reconnecting = false; + } + } + + private async global::System.Threading.Tasks.Task ReconnectSynchronized( + ReconnectionType type, + bool failFast, + Exception? causedException + ) + { + using (await _locker.LockAsync()) + { + await Reconnect(type, failFast, causedException); + } + } + + private async global::System.Threading.Tasks.Task Reconnect( + ReconnectionType type, + bool failFast, + Exception? causedException + ) + { + IsRunning = false; + if (_disposing || !IsStarted) + return; + + _reconnecting = true; + + var disType = ToDisconnectionType(type); + var disInfo = DisconnectionInfo.Create(disType, _client, causedException); + if ( + type != ReconnectionType.Error + && _client?.State != WebSocketState.CloseReceived + && _client?.State != WebSocketState.Closed + ) + { + await OnDisconnectionHappened(disInfo).ConfigureAwait(false); + if (disInfo.CancelReconnection) + { + _reconnecting = false; + return; + } + } + + _cancellation?.Cancel(); + try + { + _client?.Abort(); + } + catch + { /* ignored */ + } + _client?.Dispose(); + + if ( + type != ReconnectionType.Error + && (!_isReconnectionEnabled || disInfo.CancelReconnection) + ) + { + IsStarted = false; + _reconnecting = false; + return; + } + + // Consult backoff strategy for delay + if (Backoff != null) + { + var delay = Backoff.GetNextDelay(); + if (delay == null) + { + // Max attempts exhausted + _logger.LogWarning( + "Reconnection attempts exhausted after {Attempts} tries", + Backoff.MaxAttempts + ); + IsStarted = false; + _reconnecting = false; + var info = DisconnectionInfo.Create(DisconnectionType.Exit, null, causedException); + await OnDisconnectionHappened(info).ConfigureAwait(false); + return; + } + + _logger.LogInformation( + "Reconnecting in {Delay}ms (attempt {Attempt})", + delay.Value.TotalMilliseconds, + Backoff.MaxAttempts.HasValue + ? $"{Backoff.AttemptsMade}/{Backoff.MaxAttempts}" + : $"{Backoff.AttemptsMade}" + ); + + try + { + await global::System + .Threading.Tasks.Task.Delay( + delay.Value, + _cancellationTotal?.Token ?? CancellationToken.None + ) + .ConfigureAwait(false); + } + catch (OperationCanceledException) + { + _reconnecting = false; + return; + } + } + + _cancellation = new CancellationTokenSource(); + await StartClient(Url, _cancellation.Token, failFast).ConfigureAwait(false); + if (IsRunning) + { + // Reset backoff on successful reconnection + Backoff?.Reset(); + await OnReconnectionHappened(ReconnectionInfo.Create(type)).ConfigureAwait(false); + } + _reconnecting = false; + } + + private bool ShouldIgnoreReconnection(WebSocket client) + { + var inProgress = _disposing || _reconnecting || _stopping; + var differentClient = client != _client; + return inProgress || differentClient; + } + + private static DisconnectionType ToDisconnectionType(ReconnectionType type) => + type switch + { + ReconnectionType.Initial => DisconnectionType.Exit, + ReconnectionType.Lost => DisconnectionType.Lost, + ReconnectionType.NoMessageReceived => DisconnectionType.NoMessageReceived, + ReconnectionType.Error => DisconnectionType.Error, + ReconnectionType.ByUser => DisconnectionType.ByUser, + ReconnectionType.ByServer => DisconnectionType.ByServer, + _ => DisconnectionType.Exit, + }; + + private void ActivateLastChance() + { + var timerMs = 1000; + _lastChanceTimer = new Timer(LastChance, null, timerMs, timerMs); + } + + private void DeactivateLastChance() + { + _lastChanceTimer?.Dispose(); + _lastChanceTimer = null; + } + + private void LastChance(object? state) + { + if (!_isReconnectionEnabled || ReconnectTimeout == null) + { + DeactivateLastChance(); + return; + } + + var timeoutMs = Math.Abs(ReconnectTimeout.Value.TotalMilliseconds); + var diffMs = Math.Abs(DateTime.UtcNow.Subtract(_lastReceivedMsg).TotalMilliseconds); + if (diffMs > timeoutMs) + { + DeactivateLastChance(); + _ = ReconnectSynchronized(ReconnectionType.NoMessageReceived, false, null); } } diff --git a/src/Hume/Core/WithRawResponse.cs b/src/Hume/Core/WithRawResponse.cs deleted file mode 100644 index 1ba19bf..0000000 --- a/src/Hume/Core/WithRawResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Hume.Core; - -/// -/// Wraps a parsed response value with its raw HTTP response metadata. -/// -/// The type of the parsed response data. -public readonly struct WithRawResponse -{ - /// - /// The parsed response data. - /// - public required T Data { get; init; } - - /// - /// The raw HTTP response metadata. - /// - public required RawResponse RawResponse { get; init; } -} diff --git a/src/Hume/Core/WithRawResponseTask.cs b/src/Hume/Core/WithRawResponseTask.cs deleted file mode 100644 index 46414fa..0000000 --- a/src/Hume/Core/WithRawResponseTask.cs +++ /dev/null @@ -1,144 +0,0 @@ -using global::System.Runtime.CompilerServices; - -namespace Hume.Core; - -/// -/// A task-like type that wraps Task<WithRawResponse<T>> and provides dual-mode awaiting: -/// - Direct await yields just T (zero-allocation path for common case) -/// - .WithRawResponse() yields WithRawResponse<T> (when raw response metadata is needed) -/// -/// The type of the parsed response data. -public readonly struct WithRawResponseTask -{ - private readonly global::System.Threading.Tasks.Task> _task; - - /// - /// Creates a new WithRawResponseTask wrapping the given task. - /// - public WithRawResponseTask(global::System.Threading.Tasks.Task> task) - { - _task = task; - } - - /// - /// Returns the underlying task that yields both the data and raw response metadata. - /// - public global::System.Threading.Tasks.Task> WithRawResponse() => _task; - - /// - /// Gets the custom awaiter that unwraps to just T when awaited. - /// - public Awaiter GetAwaiter() => new(_task.GetAwaiter()); - - /// - /// Configures the awaiter to continue on the captured context or not. - /// - public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) => - new(_task.ConfigureAwait(continueOnCapturedContext)); - - /// - /// Implicitly converts WithRawResponseTask<T> to global::System.Threading.Tasks.Task<T> for backward compatibility. - /// The resulting task will yield just the data when awaited. - /// - public static implicit operator global::System.Threading.Tasks.Task( - WithRawResponseTask task - ) - { - return task._task.ContinueWith( - t => t.Result.Data, - TaskContinuationOptions.ExecuteSynchronously - ); - } - - /// - /// Custom awaiter that unwraps WithRawResponse<T> to just T. - /// - public readonly struct Awaiter : ICriticalNotifyCompletion - { - private readonly TaskAwaiter> _awaiter; - - internal Awaiter(TaskAwaiter> awaiter) - { - _awaiter = awaiter; - } - - /// - /// Gets whether the underlying task has completed. - /// - public bool IsCompleted => _awaiter.IsCompleted; - - /// - /// Gets the result, unwrapping to just the data. - /// - public T GetResult() => _awaiter.GetResult().Data; - - /// - /// Schedules the continuation action. - /// - public void OnCompleted(global::System.Action continuation) => - _awaiter.OnCompleted(continuation); - - /// - /// Schedules the continuation action without capturing the execution context. - /// - public void UnsafeOnCompleted(global::System.Action continuation) => - _awaiter.UnsafeOnCompleted(continuation); - } - - /// - /// Awaitable type returned by ConfigureAwait that unwraps to just T. - /// - public readonly struct ConfiguredTaskAwaitable - { - private readonly ConfiguredTaskAwaitable> _configuredTask; - - internal ConfiguredTaskAwaitable(ConfiguredTaskAwaitable> configuredTask) - { - _configuredTask = configuredTask; - } - - /// - /// Gets the configured awaiter that unwraps to just T. - /// - public ConfiguredAwaiter GetAwaiter() => new(_configuredTask.GetAwaiter()); - - /// - /// Custom configured awaiter that unwraps WithRawResponse<T> to just T. - /// - public readonly struct ConfiguredAwaiter : ICriticalNotifyCompletion - { - private readonly ConfiguredTaskAwaitable< - WithRawResponse - >.ConfiguredTaskAwaiter _awaiter; - - internal ConfiguredAwaiter( - ConfiguredTaskAwaitable>.ConfiguredTaskAwaiter awaiter - ) - { - _awaiter = awaiter; - } - - /// - /// Gets whether the underlying task has completed. - /// - public bool IsCompleted => _awaiter.IsCompleted; - - /// - /// Gets the result, unwrapping to just the data. - /// - public T GetResult() => _awaiter.GetResult().Data; - - /// - /// Schedules the continuation action. - /// - public void OnCompleted(global::System.Action continuation) => - _awaiter.OnCompleted(continuation); - - /// - /// Schedules the continuation action without capturing the execution context. - /// - public void UnsafeOnCompleted(global::System.Action continuation) => - _awaiter.UnsafeOnCompleted(continuation); - } - } -} diff --git a/src/Hume/EmpathicVoice/Chat/ChatApi.cs b/src/Hume/EmpathicVoice/Chat/ChatApi.cs index 8f9212d..283d26b 100644 --- a/src/Hume/EmpathicVoice/Chat/ChatApi.cs +++ b/src/Hume/EmpathicVoice/Chat/ChatApi.cs @@ -1,5 +1,5 @@ -using System.ComponentModel; -using System.Text.Json; +using global::System.ComponentModel; +using global::System.Text.Json; using Hume.Core; using Hume.Core.WebSockets; using OneOf; @@ -9,7 +9,7 @@ namespace Hume.EmpathicVoice; /// /// Chat with Empathic Voice Interface (EVI) /// -public partial class ChatApi : IAsyncDisposable, IDisposable, INotifyPropertyChanged +public partial class ChatApi : IChatApi, IAsyncDisposable, IDisposable, INotifyPropertyChanged { private readonly ChatApi.Options _options; @@ -96,6 +96,12 @@ public event PropertyChangedEventHandler PropertyChanged /// public readonly Event SessionSettings = new(); + /// + /// Event handler for unknown/unrecognized message types. + /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. + /// + public readonly Event UnknownMessage = new(); + /// /// Default constructor /// @@ -123,6 +129,12 @@ public ChatApi(ChatApi.Options options) }; uri.Path = $"{uri.Path.TrimEnd('/')}/chat"; _client = new WebSocketClient(uri.Uri, OnTextMessage); + _client.HttpInvoker = _options.HttpInvoker; + _client.IsReconnectionEnabled = _options.IsReconnectionEnabled; + _client.ReconnectTimeout = _options.ReconnectTimeout; + _client.ErrorReconnectTimeout = _options.ErrorReconnectTimeout; + _client.LostReconnectTimeout = _options.LostReconnectTimeout; + _client.Backoff = _options.ReconnectBackoff; } /// @@ -145,6 +157,11 @@ public ChatApi(ChatApi.Options options) /// public Event ExceptionOccurred => _client.ExceptionOccurred; + /// + /// Event raised when the WebSocket connection is re-established after a disconnect. + /// + public Event Reconnecting => _client.Reconnecting; + /// /// Disposes of event subscriptions /// @@ -162,14 +179,15 @@ private void DisposeEvents() ToolResponseMessage.Dispose(); ToolErrorMessage.Dispose(); SessionSettings.Dispose(); + UnknownMessage.Dispose(); } /// /// Dispatches incoming WebSocket messages /// - private async System.Threading.Tasks.Task OnTextMessage(Stream stream) + private async global::System.Threading.Tasks.Task OnTextMessage(Stream stream) { - var json = await JsonSerializer.DeserializeAsync(stream); + using var json = await JsonSerializer.DeserializeAsync(stream); if (json == null) { await ExceptionOccurred @@ -275,25 +293,54 @@ await ExceptionOccurred } } - await ExceptionOccurred - .RaiseEvent(new Exception($"Unknown message: {json.ToString()}")) + await UnknownMessage.RaiseEvent(json.RootElement.Clone()).ConfigureAwait(false); + } + + /// + /// Serializes and sends a JSON message to the server + /// + private async global::System.Threading.Tasks.Task SendJsonAsync( + object message, + CancellationToken cancellationToken = default + ) + { + await _client + .SendInstant(JsonUtils.Serialize(message), cancellationToken) .ConfigureAwait(false); } + /// + /// Injects a fake text message for testing. Dispatches through the normal message handling pipeline. + /// + internal async global::System.Threading.Tasks.Task InjectTestMessage(string rawJson) + { + using var stream = new MemoryStream(global::System.Text.Encoding.UTF8.GetBytes(rawJson)); + await OnTextMessage(stream).ConfigureAwait(false); + } + /// /// Asynchronously establishes a WebSocket connection. /// - public async System.Threading.Tasks.Task ConnectAsync() + public async global::System.Threading.Tasks.Task ConnectAsync( + CancellationToken cancellationToken = default + ) { - await _client.ConnectAsync().ConfigureAwait(false); +#if NET6_0_OR_GREATER + _client.DeflateOptions = _options.EnableCompression + ? new System.Net.WebSockets.WebSocketDeflateOptions() + : null; +#endif + await _client.ConnectAsync(cancellationToken).ConfigureAwait(false); } /// /// Asynchronously closes the WebSocket connection. /// - public async System.Threading.Tasks.Task CloseAsync() + public async global::System.Threading.Tasks.Task CloseAsync( + CancellationToken cancellationToken = default + ) { - await _client.CloseAsync().ConfigureAwait(false); + await _client.CloseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -319,7 +366,7 @@ public void Dispose() /// /// Sends a PublishEvent message to the server /// - public async System.Threading.Tasks.Task Send( + public async global::System.Threading.Tasks.Task Send( OneOf< AudioInput, SessionSettings, @@ -329,10 +376,11 @@ public async System.Threading.Tasks.Task Send( ToolErrorMessage, PauseAssistantMessage, ResumeAssistantMessage - > message + > message, + CancellationToken cancellationToken = default ) { - await _client.SendInstant(JsonUtils.Serialize(message)).ConfigureAwait(false); + await SendJsonAsync(message, cancellationToken).ConfigureAwait(false); } /// @@ -345,6 +393,17 @@ public class Options /// public string BaseUrl { get; set; } = "wss://api.hume.ai/v0/evi"; + /// + /// Enable per-message deflate compression (RFC 7692). When true, the client sets ClientWebSocketOptions.DangerousDeflateOptions before connecting. Compression is negotiated during the handshake; if the server does not support it, the connection proceeds uncompressed. Default: false. + /// Security warning: do not enable compression when transmitting data containing secrets — compressed encrypted payloads are vulnerable to CRIME/BREACH side-channel attacks. See ClientWebSocketOptions.DangerousDeflateOptions for details. + /// + public bool EnableCompression { get; set; } = false; + + /// + /// Optional HTTP/2 handler for multiplexed WebSocket connections (.NET 7+). + /// + public System.Net.Http.HttpMessageInvoker? HttpInvoker { get; set; } + /// /// Access token used for authenticating the client. If not provided, an `api_key` must be provided to authenticate. /// @@ -407,5 +466,30 @@ public class Options public string? ApiKey { get; set; } public ConnectSessionSettings? SessionSettings { get; set; } + + /// + /// Enable or disable automatic reconnection. Default: false. + /// + public bool IsReconnectionEnabled { get; set; } = false; + + /// + /// Time to wait before reconnecting if no message comes from the server. Set null to disable. Default: 1 minute. + /// + public TimeSpan? ReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the last reconnection attempt failed. Set null to disable. Default: 1 minute. + /// + public TimeSpan? ErrorReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the connection is lost with a transient error. Set null to disable (reconnect immediately). Default: null. + /// + public TimeSpan? LostReconnectTimeout { get; set; } + + /// + /// Backoff strategy for reconnection delays. Controls interval growth, jitter, and max attempts. Set to null to use fixed-interval reconnection (legacy behavior). Default: exponential backoff, 1s→60s, unlimited attempts, with jitter. + /// + public ReconnectStrategy? ReconnectBackoff { get; set; } = new ReconnectStrategy(); } } diff --git a/src/Hume/EmpathicVoice/Chat/IChatApi.cs b/src/Hume/EmpathicVoice/Chat/IChatApi.cs new file mode 100644 index 0000000..28a6a33 --- /dev/null +++ b/src/Hume/EmpathicVoice/Chat/IChatApi.cs @@ -0,0 +1,30 @@ +using Hume.Core.WebSockets; +using OneOf; + +namespace Hume.EmpathicVoice; + +public partial interface IChatApi : IAsyncDisposable, IDisposable +{ + public Event Connected { get; } + public Event Closed { get; } + public Event ExceptionOccurred { get; } + public Event Reconnecting { get; } + public ConnectionStatus Status { get; } + global::System.Threading.Tasks.Task ConnectAsync(CancellationToken cancellationToken = default); + + global::System.Threading.Tasks.Task Send( + OneOf< + AudioInput, + SessionSettings, + UserInput, + AssistantInput, + ToolResponseMessage, + ToolErrorMessage, + PauseAssistantMessage, + ResumeAssistantMessage + > message, + CancellationToken cancellationToken = default + ); + + global::System.Threading.Tasks.Task CloseAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Hume/EmpathicVoice/ChatGroups/ChatGroupsClient.cs b/src/Hume/EmpathicVoice/ChatGroups/ChatGroupsClient.cs index e14ba97..21e8f24 100644 --- a/src/Hume/EmpathicVoice/ChatGroups/ChatGroupsClient.cs +++ b/src/Hume/EmpathicVoice/ChatGroups/ChatGroupsClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; @@ -6,7 +6,7 @@ namespace Hume.EmpathicVoice; public partial class ChatGroupsClient : IChatGroupsClient { - private RawClient _client; + private readonly RawClient _client; internal ChatGroupsClient(RawClient client) { @@ -27,7 +27,7 @@ private WithRawResponseTask ListChatGroupsInternalAsync( ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListChatGroupsInternalAsyncCore( ChatGroupsListChatGroupsRequest request, @@ -64,7 +64,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -90,7 +92,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -113,7 +117,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > GetChatGroupAsyncCore( string id, @@ -154,7 +158,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -180,7 +186,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -203,7 +211,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > GetAudioAsyncCore( string id, @@ -243,7 +251,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize( @@ -271,7 +281,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -309,7 +321,7 @@ private WithRawResponseTask ListChatGroupEventsInter ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListChatGroupEventsInternalAsyncCore( string id, @@ -349,7 +361,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -375,7 +389,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -412,7 +428,7 @@ private async System.Threading.Tasks.Task< /// } /// ); /// - public async System.Threading.Tasks.Task> ListChatGroupsAsync( + public async global::System.Threading.Tasks.Task> ListChatGroupsAsync( ChatGroupsListChatGroupsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -431,7 +447,8 @@ public async System.Threading.Tasks.Task> ListChatGroupsA request, options, async (request, options, cancellationToken) => - await ListChatGroupsInternalAsync(request, options, cancellationToken), + await ListChatGroupsInternalAsync(request, options, cancellationToken) + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { @@ -512,7 +529,9 @@ public WithRawResponseTask GetAudioAsy /// } /// ); /// - public async System.Threading.Tasks.Task> ListChatGroupEventsAsync( + public async global::System.Threading.Tasks.Task< + Pager + > ListChatGroupEventsAsync( string id, ChatGroupsListChatGroupEventsRequest request, RequestOptions? options = null, @@ -533,7 +552,7 @@ public async System.Threading.Tasks.Task> ListChatGroupEv options, async (request, options, cancellationToken) => await ListChatGroupEventsInternalAsync(id, request, options, cancellationToken) - .ConfigureAwait(false), + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { diff --git a/src/Hume/EmpathicVoice/ChatGroups/IChatGroupsClient.cs b/src/Hume/EmpathicVoice/ChatGroups/IChatGroupsClient.cs index 10f8d02..790ba42 100644 --- a/src/Hume/EmpathicVoice/ChatGroups/IChatGroupsClient.cs +++ b/src/Hume/EmpathicVoice/ChatGroups/IChatGroupsClient.cs @@ -8,7 +8,7 @@ public partial interface IChatGroupsClient /// /// Fetches a paginated list of **Chat Groups**. /// - System.Threading.Tasks.Task> ListChatGroupsAsync( + global::System.Threading.Tasks.Task> ListChatGroupsAsync( ChatGroupsListChatGroupsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -37,7 +37,7 @@ WithRawResponseTask GetAudioAsync( /// /// Fetches a paginated list of **Chat** events associated with a **Chat Group**. /// - System.Threading.Tasks.Task> ListChatGroupEventsAsync( + global::System.Threading.Tasks.Task> ListChatGroupEventsAsync( string id, ChatGroupsListChatGroupEventsRequest request, RequestOptions? options = null, diff --git a/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsGetAudioRequest.cs b/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsGetAudioRequest.cs index 82a2226..0699c7c 100644 --- a/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsGetAudioRequest.cs +++ b/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsGetAudioRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsGetChatGroupRequest.cs b/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsGetChatGroupRequest.cs index c7656fe..616dd83 100644 --- a/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsGetChatGroupRequest.cs +++ b/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsGetChatGroupRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsListChatGroupEventsRequest.cs b/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsListChatGroupEventsRequest.cs index 6412f05..5ad1f87 100644 --- a/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsListChatGroupEventsRequest.cs +++ b/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsListChatGroupEventsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsListChatGroupsRequest.cs b/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsListChatGroupsRequest.cs index 04d6c64..5145b63 100644 --- a/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsListChatGroupsRequest.cs +++ b/src/Hume/EmpathicVoice/ChatGroups/Requests/ChatGroupsListChatGroupsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Chats/ChatsClient.cs b/src/Hume/EmpathicVoice/Chats/ChatsClient.cs index 9b66839..4ba7ee0 100644 --- a/src/Hume/EmpathicVoice/Chats/ChatsClient.cs +++ b/src/Hume/EmpathicVoice/Chats/ChatsClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; @@ -6,7 +6,7 @@ namespace Hume.EmpathicVoice; public partial class ChatsClient : IChatsClient { - private RawClient _client; + private readonly RawClient _client; internal ChatsClient(RawClient client) { @@ -27,7 +27,7 @@ private WithRawResponseTask ListChatsInternalAsync( ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListChatsInternalAsyncCore( ChatsListChatsRequest request, @@ -65,7 +65,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -91,7 +93,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -129,7 +133,7 @@ private WithRawResponseTask ListChatEventsInternalAsync( ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListChatEventsInternalAsyncCore( string id, @@ -169,7 +173,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -195,7 +201,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -218,7 +226,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > GetAudioAsyncCore( string id, @@ -250,7 +258,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize( @@ -278,7 +288,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -314,7 +326,7 @@ private async System.Threading.Tasks.Task< /// } /// ); /// - public async System.Threading.Tasks.Task> ListChatsAsync( + public async global::System.Threading.Tasks.Task> ListChatsAsync( ChatsListChatsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -333,7 +345,8 @@ public async System.Threading.Tasks.Task> ListChatsAsync( request, options, async (request, options, cancellationToken) => - await ListChatsInternalAsync(request, options, cancellationToken), + await ListChatsInternalAsync(request, options, cancellationToken) + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { @@ -362,7 +375,7 @@ await ListChatsInternalAsync(request, options, cancellationToken), /// } /// ); /// - public async System.Threading.Tasks.Task> ListChatEventsAsync( + public async global::System.Threading.Tasks.Task> ListChatEventsAsync( string id, ChatsListChatEventsRequest request, RequestOptions? options = null, @@ -383,7 +396,7 @@ public async System.Threading.Tasks.Task> ListChatEventsA options, async (request, options, cancellationToken) => await ListChatEventsInternalAsync(id, request, options, cancellationToken) - .ConfigureAwait(false), + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { diff --git a/src/Hume/EmpathicVoice/Chats/IChatsClient.cs b/src/Hume/EmpathicVoice/Chats/IChatsClient.cs index fea2836..5832cf4 100644 --- a/src/Hume/EmpathicVoice/Chats/IChatsClient.cs +++ b/src/Hume/EmpathicVoice/Chats/IChatsClient.cs @@ -8,7 +8,7 @@ public partial interface IChatsClient /// /// Fetches a paginated list of **Chats**. /// - System.Threading.Tasks.Task> ListChatsAsync( + global::System.Threading.Tasks.Task> ListChatsAsync( ChatsListChatsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -17,7 +17,7 @@ System.Threading.Tasks.Task> ListChatsAsync( /// /// Fetches a paginated list of **Chat** events. /// - System.Threading.Tasks.Task> ListChatEventsAsync( + global::System.Threading.Tasks.Task> ListChatEventsAsync( string id, ChatsListChatEventsRequest request, RequestOptions? options = null, diff --git a/src/Hume/EmpathicVoice/Chats/Requests/ChatsListChatEventsRequest.cs b/src/Hume/EmpathicVoice/Chats/Requests/ChatsListChatEventsRequest.cs index 0f8f723..c41b81f 100644 --- a/src/Hume/EmpathicVoice/Chats/Requests/ChatsListChatEventsRequest.cs +++ b/src/Hume/EmpathicVoice/Chats/Requests/ChatsListChatEventsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Chats/Requests/ChatsListChatsRequest.cs b/src/Hume/EmpathicVoice/Chats/Requests/ChatsListChatsRequest.cs index 458e774..e67edad 100644 --- a/src/Hume/EmpathicVoice/Chats/Requests/ChatsListChatsRequest.cs +++ b/src/Hume/EmpathicVoice/Chats/Requests/ChatsListChatsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Configs/ConfigsClient.cs b/src/Hume/EmpathicVoice/Configs/ConfigsClient.cs index a37b01b..46ee836 100644 --- a/src/Hume/EmpathicVoice/Configs/ConfigsClient.cs +++ b/src/Hume/EmpathicVoice/Configs/ConfigsClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; @@ -6,7 +6,7 @@ namespace Hume.EmpathicVoice; public partial class ConfigsClient : IConfigsClient { - private RawClient _client; + private readonly RawClient _client; internal ConfigsClient(RawClient client) { @@ -29,7 +29,7 @@ private WithRawResponseTask ListConfigsInternalAsync( ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListConfigsInternalAsyncCore( ConfigsListConfigsRequest request, @@ -66,7 +66,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -92,7 +94,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -115,7 +119,9 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task> CreateConfigAsyncCore( + private async global::System.Threading.Tasks.Task< + WithRawResponse + > CreateConfigAsyncCore( PostedConfig request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -144,7 +150,9 @@ private async System.Threading.Tasks.Task> CreateC .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -170,7 +178,9 @@ private async System.Threading.Tasks.Task> CreateC } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -210,7 +220,7 @@ private WithRawResponseTask ListConfigVersionsInternalAsync( ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListConfigVersionsInternalAsyncCore( string id, @@ -250,7 +260,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -276,7 +288,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -299,7 +313,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > CreateConfigVersionAsyncCore( string id, @@ -334,7 +348,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -360,7 +376,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -383,7 +401,9 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task> UpdateConfigNameAsyncCore( + private async global::System.Threading.Tasks.Task< + WithRawResponse + > UpdateConfigNameAsyncCore( string id, PostedConfigName request, RequestOptions? options = null, @@ -416,7 +436,9 @@ private async System.Threading.Tasks.Task> UpdateConfigN .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); return new WithRawResponse() { Data = responseBody, @@ -429,7 +451,9 @@ private async System.Threading.Tasks.Task> UpdateConfigN }; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -452,7 +476,7 @@ private async System.Threading.Tasks.Task> UpdateConfigN } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > GetConfigVersionAsyncCore( string id, @@ -486,7 +510,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -512,7 +538,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -535,7 +563,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > UpdateConfigDescriptionAsyncCore( string id, @@ -572,7 +600,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -598,7 +628,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -631,7 +663,7 @@ private async System.Threading.Tasks.Task< /// new ConfigsListConfigsRequest { PageNumber = 0, PageSize = 1 } /// ); /// - public async System.Threading.Tasks.Task> ListConfigsAsync( + public async global::System.Threading.Tasks.Task> ListConfigsAsync( ConfigsListConfigsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -650,7 +682,8 @@ public async System.Threading.Tasks.Task> ListConfigsAsync( request, options, async (request, options, cancellationToken) => - await ListConfigsInternalAsync(request, options, cancellationToken), + await ListConfigsInternalAsync(request, options, cancellationToken) + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { @@ -719,7 +752,7 @@ public WithRawResponseTask CreateConfigAsync( /// new ConfigsListConfigVersionsRequest() /// ); /// - public async System.Threading.Tasks.Task> ListConfigVersionsAsync( + public async global::System.Threading.Tasks.Task> ListConfigVersionsAsync( string id, ConfigsListConfigVersionsRequest request, RequestOptions? options = null, @@ -740,7 +773,7 @@ public async System.Threading.Tasks.Task> ListConfigVersions options, async (request, options, cancellationToken) => await ListConfigVersionsInternalAsync(id, request, options, cancellationToken) - .ConfigureAwait(false), + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { @@ -809,7 +842,7 @@ public WithRawResponseTask CreateConfigVersionAsync( /// /// await client.EmpathicVoice.Configs.DeleteConfigAsync("your-config-id"); /// - public async System.Threading.Tasks.Task DeleteConfigAsync( + public async global::System.Threading.Tasks.Task DeleteConfigAsync( string id, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -842,7 +875,9 @@ public async System.Threading.Tasks.Task DeleteConfigAsync( return; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -916,7 +951,7 @@ public WithRawResponseTask GetConfigVersionAsync( /// /// await client.EmpathicVoice.Configs.DeleteConfigVersionAsync("your-config-id", 1); /// - public async System.Threading.Tasks.Task DeleteConfigVersionAsync( + public async global::System.Threading.Tasks.Task DeleteConfigVersionAsync( string id, int version, RequestOptions? options = null, @@ -951,7 +986,9 @@ public async System.Threading.Tasks.Task DeleteConfigVersionAsync( return; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) diff --git a/src/Hume/EmpathicVoice/Configs/IConfigsClient.cs b/src/Hume/EmpathicVoice/Configs/IConfigsClient.cs index c8fdfec..d964f25 100644 --- a/src/Hume/EmpathicVoice/Configs/IConfigsClient.cs +++ b/src/Hume/EmpathicVoice/Configs/IConfigsClient.cs @@ -10,7 +10,7 @@ public partial interface IConfigsClient /// /// For more details on configuration options and how to configure EVI, see our [configuration guide](/docs/speech-to-speech-evi/configuration). /// - System.Threading.Tasks.Task> ListConfigsAsync( + global::System.Threading.Tasks.Task> ListConfigsAsync( ConfigsListConfigsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -32,7 +32,7 @@ WithRawResponseTask CreateConfigAsync( /// /// For more details on configuration options and how to configure EVI, see our [configuration guide](/docs/speech-to-speech-evi/configuration). /// - System.Threading.Tasks.Task> ListConfigVersionsAsync( + global::System.Threading.Tasks.Task> ListConfigVersionsAsync( string id, ConfigsListConfigVersionsRequest request, RequestOptions? options = null, @@ -56,7 +56,7 @@ WithRawResponseTask CreateConfigVersionAsync( /// /// For more details on configuration options and how to configure EVI, see our [configuration guide](/docs/speech-to-speech-evi/configuration). /// - System.Threading.Tasks.Task DeleteConfigAsync( + global::System.Threading.Tasks.Task DeleteConfigAsync( string id, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -91,7 +91,7 @@ WithRawResponseTask GetConfigVersionAsync( /// /// For more details on configuration options and how to configure EVI, see our [configuration guide](/docs/speech-to-speech-evi/configuration). /// - System.Threading.Tasks.Task DeleteConfigVersionAsync( + global::System.Threading.Tasks.Task DeleteConfigVersionAsync( string id, int version, RequestOptions? options = null, diff --git a/src/Hume/EmpathicVoice/Configs/Requests/ConfigsListConfigVersionsRequest.cs b/src/Hume/EmpathicVoice/Configs/Requests/ConfigsListConfigVersionsRequest.cs index 8ae040f..013e843 100644 --- a/src/Hume/EmpathicVoice/Configs/Requests/ConfigsListConfigVersionsRequest.cs +++ b/src/Hume/EmpathicVoice/Configs/Requests/ConfigsListConfigVersionsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Configs/Requests/ConfigsListConfigsRequest.cs b/src/Hume/EmpathicVoice/Configs/Requests/ConfigsListConfigsRequest.cs index 24adb9e..b34c761 100644 --- a/src/Hume/EmpathicVoice/Configs/Requests/ConfigsListConfigsRequest.cs +++ b/src/Hume/EmpathicVoice/Configs/Requests/ConfigsListConfigsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Configs/Requests/PostedConfig.cs b/src/Hume/EmpathicVoice/Configs/Requests/PostedConfig.cs index 79badc4..db25c70 100644 --- a/src/Hume/EmpathicVoice/Configs/Requests/PostedConfig.cs +++ b/src/Hume/EmpathicVoice/Configs/Requests/PostedConfig.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; using OneOf; @@ -25,6 +25,9 @@ public record PostedConfig [JsonPropertyName("evi_version")] public required string EviVersion { get; set; } + [JsonPropertyName("interruption")] + public PostedInterruptionSpec? Interruption { get; set; } + [JsonPropertyName("language_model")] public PostedLanguageModel? LanguageModel { get; set; } @@ -49,6 +52,9 @@ public record PostedConfig [JsonPropertyName("tools")] public IEnumerable? Tools { get; set; } + [JsonPropertyName("turn_detection")] + public PostedTurnDetectionSpec? TurnDetection { get; set; } + /// /// An optional description of the Config version. /// diff --git a/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigName.cs b/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigName.cs index 4a9b8bf..384ebf7 100644 --- a/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigName.cs +++ b/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigName.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigVersion.cs b/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigVersion.cs index 03b1d4e..7141c34 100644 --- a/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigVersion.cs +++ b/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigVersion.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; using OneOf; @@ -25,6 +25,9 @@ public record PostedConfigVersion [JsonPropertyName("evi_version")] public required string EviVersion { get; set; } + [JsonPropertyName("interruption")] + public PostedInterruptionSpec? Interruption { get; set; } + [JsonPropertyName("language_model")] public PostedLanguageModel? LanguageModel { get; set; } @@ -43,6 +46,9 @@ public record PostedConfigVersion [JsonPropertyName("tools")] public IEnumerable? Tools { get; set; } + [JsonPropertyName("turn_detection")] + public PostedTurnDetectionSpec? TurnDetection { get; set; } + /// /// An optional description of the Config version. /// diff --git a/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigVersionDescription.cs b/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigVersionDescription.cs index d373f59..1b7e16f 100644 --- a/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigVersionDescription.cs +++ b/src/Hume/EmpathicVoice/Configs/Requests/PostedConfigVersionDescription.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneApi.cs b/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneApi.cs index 5c04bd6..3efe56d 100644 --- a/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneApi.cs +++ b/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneApi.cs @@ -1,5 +1,5 @@ -using System.ComponentModel; -using System.Text.Json; +using global::System.ComponentModel; +using global::System.Text.Json; using Hume.Core; using Hume.Core.WebSockets; using OneOf; @@ -9,7 +9,11 @@ namespace Hume.EmpathicVoice; /// /// Connects to an in-progress EVI chat session. The original chat must have been started with `allow_connection=true`. The connection can be used to send and receive the same messages as the original chat, with the exception that `audio_input` messages are not allowed. /// -public partial class ControlPlaneApi : IAsyncDisposable, IDisposable, INotifyPropertyChanged +public partial class ControlPlaneApi + : IControlPlaneApi, + IAsyncDisposable, + IDisposable, + INotifyPropertyChanged { private readonly ControlPlaneApi.Options _options; @@ -96,6 +100,12 @@ public event PropertyChangedEventHandler PropertyChanged /// public readonly Event SessionSettings = new(); + /// + /// Event handler for unknown/unrecognized message types. + /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. + /// + public readonly Event UnknownMessage = new(); + /// /// Constructor with options /// @@ -110,6 +120,12 @@ public ControlPlaneApi(ControlPlaneApi.Options options) }; uri.Path = $"{uri.Path.TrimEnd('/')}/chat/{Uri.EscapeDataString(_options.ChatId)}/connect"; _client = new WebSocketClient(uri.Uri, OnTextMessage); + _client.HttpInvoker = _options.HttpInvoker; + _client.IsReconnectionEnabled = _options.IsReconnectionEnabled; + _client.ReconnectTimeout = _options.ReconnectTimeout; + _client.ErrorReconnectTimeout = _options.ErrorReconnectTimeout; + _client.LostReconnectTimeout = _options.LostReconnectTimeout; + _client.Backoff = _options.ReconnectBackoff; } /// @@ -132,6 +148,11 @@ public ControlPlaneApi(ControlPlaneApi.Options options) /// public Event ExceptionOccurred => _client.ExceptionOccurred; + /// + /// Event raised when the WebSocket connection is re-established after a disconnect. + /// + public Event Reconnecting => _client.Reconnecting; + /// /// Disposes of event subscriptions /// @@ -149,14 +170,15 @@ private void DisposeEvents() ToolResponseMessage.Dispose(); ToolErrorMessage.Dispose(); SessionSettings.Dispose(); + UnknownMessage.Dispose(); } /// /// Dispatches incoming WebSocket messages /// - private async System.Threading.Tasks.Task OnTextMessage(Stream stream) + private async global::System.Threading.Tasks.Task OnTextMessage(Stream stream) { - var json = await JsonSerializer.DeserializeAsync(stream); + using var json = await JsonSerializer.DeserializeAsync(stream); if (json == null) { await ExceptionOccurred @@ -262,25 +284,54 @@ await ExceptionOccurred } } - await ExceptionOccurred - .RaiseEvent(new Exception($"Unknown message: {json.ToString()}")) + await UnknownMessage.RaiseEvent(json.RootElement.Clone()).ConfigureAwait(false); + } + + /// + /// Serializes and sends a JSON message to the server + /// + private async global::System.Threading.Tasks.Task SendJsonAsync( + object message, + CancellationToken cancellationToken = default + ) + { + await _client + .SendInstant(JsonUtils.Serialize(message), cancellationToken) .ConfigureAwait(false); } + /// + /// Injects a fake text message for testing. Dispatches through the normal message handling pipeline. + /// + internal async global::System.Threading.Tasks.Task InjectTestMessage(string rawJson) + { + using var stream = new MemoryStream(global::System.Text.Encoding.UTF8.GetBytes(rawJson)); + await OnTextMessage(stream).ConfigureAwait(false); + } + /// /// Asynchronously establishes a WebSocket connection. /// - public async System.Threading.Tasks.Task ConnectAsync() + public async global::System.Threading.Tasks.Task ConnectAsync( + CancellationToken cancellationToken = default + ) { - await _client.ConnectAsync().ConfigureAwait(false); +#if NET6_0_OR_GREATER + _client.DeflateOptions = _options.EnableCompression + ? new System.Net.WebSockets.WebSocketDeflateOptions() + : null; +#endif + await _client.ConnectAsync(cancellationToken).ConfigureAwait(false); } /// /// Asynchronously closes the WebSocket connection. /// - public async System.Threading.Tasks.Task CloseAsync() + public async global::System.Threading.Tasks.Task CloseAsync( + CancellationToken cancellationToken = default + ) { - await _client.CloseAsync().ConfigureAwait(false); + await _client.CloseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -306,7 +357,7 @@ public void Dispose() /// /// Sends a ControlPlanePublishEvent message to the server /// - public async System.Threading.Tasks.Task Send( + public async global::System.Threading.Tasks.Task Send( OneOf< SessionSettings, UserInput, @@ -315,10 +366,11 @@ public async System.Threading.Tasks.Task Send( ToolErrorMessage, PauseAssistantMessage, ResumeAssistantMessage - > message + > message, + CancellationToken cancellationToken = default ) { - await _client.SendInstant(JsonUtils.Serialize(message)).ConfigureAwait(false); + await SendJsonAsync(message, cancellationToken).ConfigureAwait(false); } /// @@ -329,7 +381,18 @@ public class Options /// /// The Websocket URL for the API connection. /// - public string BaseUrl { get; set; } = "evi"; + public string BaseUrl { get; set; } = "wss://api.hume.ai/v0/evi"; + + /// + /// Enable per-message deflate compression (RFC 7692). When true, the client sets ClientWebSocketOptions.DangerousDeflateOptions before connecting. Compression is negotiated during the handshake; if the server does not support it, the connection proceeds uncompressed. Default: false. + /// Security warning: do not enable compression when transmitting data containing secrets — compressed encrypted payloads are vulnerable to CRIME/BREACH side-channel attacks. See ClientWebSocketOptions.DangerousDeflateOptions for details. + /// + public bool EnableCompression { get; set; } = false; + + /// + /// Optional HTTP/2 handler for multiplexed WebSocket connections (.NET 7+). + /// + public System.Net.Http.HttpMessageInvoker? HttpInvoker { get; set; } /// /// Access token used for authenticating the client. If not provided, an `api_key` must be provided to authenticate. @@ -344,5 +407,30 @@ public class Options /// The ID of the chat to connect to. /// public required string ChatId { get; set; } + + /// + /// Enable or disable automatic reconnection. Default: false. + /// + public bool IsReconnectionEnabled { get; set; } = false; + + /// + /// Time to wait before reconnecting if no message comes from the server. Set null to disable. Default: 1 minute. + /// + public TimeSpan? ReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the last reconnection attempt failed. Set null to disable. Default: 1 minute. + /// + public TimeSpan? ErrorReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the connection is lost with a transient error. Set null to disable (reconnect immediately). Default: null. + /// + public TimeSpan? LostReconnectTimeout { get; set; } + + /// + /// Backoff strategy for reconnection delays. Controls interval growth, jitter, and max attempts. Set to null to use fixed-interval reconnection (legacy behavior). Default: exponential backoff, 1s→60s, unlimited attempts, with jitter. + /// + public ReconnectStrategy? ReconnectBackoff { get; set; } = new ReconnectStrategy(); } } diff --git a/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneClient.cs b/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneClient.cs index fef6f6d..581c31b 100644 --- a/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneClient.cs +++ b/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; using OneOf; @@ -7,7 +7,7 @@ namespace Hume.EmpathicVoice; public partial class ControlPlaneClient : IControlPlaneClient { - private RawClient _client; + private readonly RawClient _client; internal ControlPlaneClient(RawClient client) { @@ -18,12 +18,9 @@ internal ControlPlaneClient(RawClient client) /// Send a message to a specific chat. /// /// - /// await client.EmpathicVoice.ControlPlane.SendAsync( - /// "chat_id", - /// new SessionSettings { Type = "session_settings" } - /// ); + /// await client.EmpathicVoice.ControlPlane.SendAsync("chat_id", new SessionSettings()); /// - public async System.Threading.Tasks.Task SendAsync( + public async global::System.Threading.Tasks.Task SendAsync( string chatId, OneOf< SessionSettings, @@ -67,7 +64,9 @@ public async System.Threading.Tasks.Task SendAsync( return; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) diff --git a/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneApi.cs b/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneApi.cs new file mode 100644 index 0000000..9daf4df --- /dev/null +++ b/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneApi.cs @@ -0,0 +1,29 @@ +using Hume.Core.WebSockets; +using OneOf; + +namespace Hume.EmpathicVoice; + +public partial interface IControlPlaneApi : IAsyncDisposable, IDisposable +{ + public Event Connected { get; } + public Event Closed { get; } + public Event ExceptionOccurred { get; } + public Event Reconnecting { get; } + public ConnectionStatus Status { get; } + global::System.Threading.Tasks.Task ConnectAsync(CancellationToken cancellationToken = default); + + global::System.Threading.Tasks.Task Send( + OneOf< + SessionSettings, + UserInput, + AssistantInput, + ToolResponseMessage, + ToolErrorMessage, + PauseAssistantMessage, + ResumeAssistantMessage + > message, + CancellationToken cancellationToken = default + ); + + global::System.Threading.Tasks.Task CloseAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneClient.cs b/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneClient.cs index b47c755..43f3ce9 100644 --- a/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneClient.cs +++ b/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneClient.cs @@ -8,7 +8,7 @@ public partial interface IControlPlaneClient /// /// Send a message to a specific chat. /// - System.Threading.Tasks.Task SendAsync( + global::System.Threading.Tasks.Task SendAsync( string chatId, OneOf< SessionSettings, diff --git a/src/Hume/EmpathicVoice/EmpathicVoiceClient.cs b/src/Hume/EmpathicVoice/EmpathicVoiceClient.cs index 3ac29e1..db6624e 100644 --- a/src/Hume/EmpathicVoice/EmpathicVoiceClient.cs +++ b/src/Hume/EmpathicVoice/EmpathicVoiceClient.cs @@ -4,7 +4,7 @@ namespace Hume.EmpathicVoice; public partial class EmpathicVoiceClient : IEmpathicVoiceClient { - private RawClient _client; + private readonly RawClient _client; internal EmpathicVoiceClient(RawClient client) { @@ -29,17 +29,17 @@ internal EmpathicVoiceClient(RawClient client) public IToolsClient Tools { get; } - public ControlPlaneApi CreateControlPlaneApi(ControlPlaneApi.Options options) + public IControlPlaneApi CreateControlPlaneApi(ControlPlaneApi.Options options) { return new ControlPlaneApi(options); } - public ChatApi CreateChatApi() + public IChatApi CreateChatApi() { - return new ChatApi(new ChatApi.Options()); + return new ChatApi(new ChatApi.Options { BaseUrl = _client.Options.Environment.Evi }); } - public ChatApi CreateChatApi(ChatApi.Options options) + public IChatApi CreateChatApi(ChatApi.Options options) { return new ChatApi(options); } diff --git a/src/Hume/EmpathicVoice/IEmpathicVoiceClient.cs b/src/Hume/EmpathicVoice/IEmpathicVoiceClient.cs index 436decd..fbcf894 100644 --- a/src/Hume/EmpathicVoice/IEmpathicVoiceClient.cs +++ b/src/Hume/EmpathicVoice/IEmpathicVoiceClient.cs @@ -8,8 +8,9 @@ public partial interface IEmpathicVoiceClient public IConfigsClient Configs { get; } public IPromptsClient Prompts { get; } public IToolsClient Tools { get; } + IControlPlaneApi CreateControlPlaneApi(ControlPlaneApi.Options options); - public ChatApi CreateChatApi(); + IChatApi CreateChatApi(); - public ChatApi CreateChatApi(ChatApi.Options options); + IChatApi CreateChatApi(ChatApi.Options options); } diff --git a/src/Hume/EmpathicVoice/Prompts/IPromptsClient.cs b/src/Hume/EmpathicVoice/Prompts/IPromptsClient.cs index 8b1bd6e..e50ce07 100644 --- a/src/Hume/EmpathicVoice/Prompts/IPromptsClient.cs +++ b/src/Hume/EmpathicVoice/Prompts/IPromptsClient.cs @@ -10,7 +10,7 @@ public partial interface IPromptsClient /// /// See our [prompting guide](/docs/speech-to-speech-evi/guides/phone-calling) for tips on crafting your system prompt. /// - System.Threading.Tasks.Task> ListPromptsAsync( + global::System.Threading.Tasks.Task> ListPromptsAsync( PromptsListPromptsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -56,7 +56,7 @@ WithRawResponseTask ListPromptVersionsAsync( /// /// See our [prompting guide](/docs/speech-to-speech-evi/guides/phone-calling) for tips on crafting your system prompt. /// - System.Threading.Tasks.Task DeletePromptAsync( + global::System.Threading.Tasks.Task DeletePromptAsync( string id, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -91,7 +91,7 @@ WithRawResponseTask UpdatePromptNameAsync( /// /// See our [prompting guide](/docs/speech-to-speech-evi/guides/phone-calling) for tips on crafting your system prompt. /// - System.Threading.Tasks.Task DeletePromptVersionAsync( + global::System.Threading.Tasks.Task DeletePromptVersionAsync( string id, int version, RequestOptions? options = null, diff --git a/src/Hume/EmpathicVoice/Prompts/PromptsClient.cs b/src/Hume/EmpathicVoice/Prompts/PromptsClient.cs index e777807..9a8503e 100644 --- a/src/Hume/EmpathicVoice/Prompts/PromptsClient.cs +++ b/src/Hume/EmpathicVoice/Prompts/PromptsClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; @@ -6,7 +6,7 @@ namespace Hume.EmpathicVoice; public partial class PromptsClient : IPromptsClient { - private RawClient _client; + private readonly RawClient _client; internal PromptsClient(RawClient client) { @@ -29,7 +29,7 @@ private WithRawResponseTask ListPromptsInternalAsync( ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListPromptsInternalAsyncCore( PromptsListPromptsRequest request, @@ -66,7 +66,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -92,7 +94,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -115,7 +119,9 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task> CreatePromptAsyncCore( + private async global::System.Threading.Tasks.Task< + WithRawResponse + > CreatePromptAsyncCore( PostedPrompt request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -144,7 +150,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -170,7 +178,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -193,7 +203,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListPromptVersionsAsyncCore( string id, @@ -233,7 +243,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -259,7 +271,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -282,7 +296,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > CreatePromptVersionAsyncCore( string id, @@ -317,7 +331,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -343,7 +359,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -366,7 +384,9 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task> UpdatePromptNameAsyncCore( + private async global::System.Threading.Tasks.Task< + WithRawResponse + > UpdatePromptNameAsyncCore( string id, PostedPromptName request, RequestOptions? options = null, @@ -399,7 +419,9 @@ private async System.Threading.Tasks.Task> UpdatePromptN .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); return new WithRawResponse() { Data = responseBody, @@ -412,7 +434,9 @@ private async System.Threading.Tasks.Task> UpdatePromptN }; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -435,7 +459,7 @@ private async System.Threading.Tasks.Task> UpdatePromptN } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > GetPromptVersionAsyncCore( string id, @@ -469,7 +493,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -495,7 +521,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -518,7 +546,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > UpdatePromptDescriptionAsyncCore( string id, @@ -555,7 +583,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -581,7 +611,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -614,7 +646,7 @@ private async System.Threading.Tasks.Task< /// new PromptsListPromptsRequest { PageNumber = 0, PageSize = 2 } /// ); /// - public async System.Threading.Tasks.Task> ListPromptsAsync( + public async global::System.Threading.Tasks.Task> ListPromptsAsync( PromptsListPromptsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -633,7 +665,8 @@ public async System.Threading.Tasks.Task> ListPromptsAsync( request, options, async (request, options, cancellationToken) => - await ListPromptsInternalAsync(request, options, cancellationToken), + await ListPromptsInternalAsync(request, options, cancellationToken) + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { @@ -733,7 +766,7 @@ public WithRawResponseTask ListPromptVersionsAsync( /// /// await client.EmpathicVoice.Prompts.DeletePromptAsync("your-prompt-id"); /// - public async System.Threading.Tasks.Task DeletePromptAsync( + public async global::System.Threading.Tasks.Task DeletePromptAsync( string id, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -766,7 +799,9 @@ public async System.Threading.Tasks.Task DeletePromptAsync( return; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -840,7 +875,7 @@ public WithRawResponseTask UpdatePromptNameAsync( /// /// await client.EmpathicVoice.Prompts.DeletePromptVersionAsync("your-prompt-id", 1); /// - public async System.Threading.Tasks.Task DeletePromptVersionAsync( + public async global::System.Threading.Tasks.Task DeletePromptVersionAsync( string id, int version, RequestOptions? options = null, @@ -875,7 +910,9 @@ public async System.Threading.Tasks.Task DeletePromptVersionAsync( return; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) diff --git a/src/Hume/EmpathicVoice/Prompts/Requests/PostedPrompt.cs b/src/Hume/EmpathicVoice/Prompts/Requests/PostedPrompt.cs index e594b21..fce9e22 100644 --- a/src/Hume/EmpathicVoice/Prompts/Requests/PostedPrompt.cs +++ b/src/Hume/EmpathicVoice/Prompts/Requests/PostedPrompt.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptName.cs b/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptName.cs index 52950f7..4118dfc 100644 --- a/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptName.cs +++ b/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptName.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptVersion.cs b/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptVersion.cs index 85c76d1..fe480a0 100644 --- a/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptVersion.cs +++ b/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptVersion.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptVersionDescription.cs b/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptVersionDescription.cs index 7091b99..086aabb 100644 --- a/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptVersionDescription.cs +++ b/src/Hume/EmpathicVoice/Prompts/Requests/PostedPromptVersionDescription.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Prompts/Requests/PromptsListPromptVersionsRequest.cs b/src/Hume/EmpathicVoice/Prompts/Requests/PromptsListPromptVersionsRequest.cs index 5ca3797..5c02ee4 100644 --- a/src/Hume/EmpathicVoice/Prompts/Requests/PromptsListPromptVersionsRequest.cs +++ b/src/Hume/EmpathicVoice/Prompts/Requests/PromptsListPromptVersionsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Prompts/Requests/PromptsListPromptsRequest.cs b/src/Hume/EmpathicVoice/Prompts/Requests/PromptsListPromptsRequest.cs index bc7742e..5f4c555 100644 --- a/src/Hume/EmpathicVoice/Prompts/Requests/PromptsListPromptsRequest.cs +++ b/src/Hume/EmpathicVoice/Prompts/Requests/PromptsListPromptsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Tools/IToolsClient.cs b/src/Hume/EmpathicVoice/Tools/IToolsClient.cs index 6cf5715..91a0c6a 100644 --- a/src/Hume/EmpathicVoice/Tools/IToolsClient.cs +++ b/src/Hume/EmpathicVoice/Tools/IToolsClient.cs @@ -10,7 +10,7 @@ public partial interface IToolsClient /// /// Refer to our [tool use](/docs/speech-to-speech-evi/features/tool-use#function-calling) guide for comprehensive instructions on defining and integrating tools into EVI. /// - System.Threading.Tasks.Task> ListToolsAsync( + global::System.Threading.Tasks.Task> ListToolsAsync( ToolsListToolsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -32,7 +32,7 @@ System.Threading.Tasks.Task> ListToolsAsync( /// /// Refer to our [tool use](/docs/speech-to-speech-evi/features/tool-use#function-calling) guide for comprehensive instructions on defining and integrating tools into EVI. /// - System.Threading.Tasks.Task> ListToolVersionsAsync( + global::System.Threading.Tasks.Task> ListToolVersionsAsync( string id, ToolsListToolVersionsRequest request, RequestOptions? options = null, @@ -56,7 +56,7 @@ System.Threading.Tasks.Task> ListToolVersionsAsync( /// /// Refer to our [tool use](/docs/speech-to-speech-evi/features/tool-use#function-calling) guide for comprehensive instructions on defining and integrating tools into EVI. /// - System.Threading.Tasks.Task DeleteToolAsync( + global::System.Threading.Tasks.Task DeleteToolAsync( string id, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -91,7 +91,7 @@ WithRawResponseTask UpdateToolNameAsync( /// /// Refer to our [tool use](/docs/speech-to-speech-evi/features/tool-use#function-calling) guide for comprehensive instructions on defining and integrating tools into EVI. /// - System.Threading.Tasks.Task DeleteToolVersionAsync( + global::System.Threading.Tasks.Task DeleteToolVersionAsync( string id, int version, RequestOptions? options = null, diff --git a/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedTool.cs b/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedTool.cs index 1513a80..bdb5d88 100644 --- a/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedTool.cs +++ b/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedTool.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolName.cs b/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolName.cs index c5877e4..74445c4 100644 --- a/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolName.cs +++ b/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolName.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolVersion.cs b/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolVersion.cs index 1f111e2..f955ee3 100644 --- a/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolVersion.cs +++ b/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolVersion.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolVersionDescription.cs b/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolVersionDescription.cs index 444aa2e..111b24d 100644 --- a/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolVersionDescription.cs +++ b/src/Hume/EmpathicVoice/Tools/Requests/PostedUserDefinedToolVersionDescription.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Tools/Requests/ToolsListToolVersionsRequest.cs b/src/Hume/EmpathicVoice/Tools/Requests/ToolsListToolVersionsRequest.cs index 709ab31..a82cf0e 100644 --- a/src/Hume/EmpathicVoice/Tools/Requests/ToolsListToolVersionsRequest.cs +++ b/src/Hume/EmpathicVoice/Tools/Requests/ToolsListToolVersionsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Tools/Requests/ToolsListToolsRequest.cs b/src/Hume/EmpathicVoice/Tools/Requests/ToolsListToolsRequest.cs index 8d04918..4219473 100644 --- a/src/Hume/EmpathicVoice/Tools/Requests/ToolsListToolsRequest.cs +++ b/src/Hume/EmpathicVoice/Tools/Requests/ToolsListToolsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; diff --git a/src/Hume/EmpathicVoice/Tools/ToolsClient.cs b/src/Hume/EmpathicVoice/Tools/ToolsClient.cs index 7168886..5f327d9 100644 --- a/src/Hume/EmpathicVoice/Tools/ToolsClient.cs +++ b/src/Hume/EmpathicVoice/Tools/ToolsClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; @@ -6,7 +6,7 @@ namespace Hume.EmpathicVoice; public partial class ToolsClient : IToolsClient { - private RawClient _client; + private readonly RawClient _client; internal ToolsClient(RawClient client) { @@ -29,7 +29,7 @@ private WithRawResponseTask ListToolsInternalAsync( ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListToolsInternalAsyncCore( ToolsListToolsRequest request, @@ -66,7 +66,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize( @@ -94,7 +96,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -117,7 +121,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > CreateToolAsyncCore( PostedUserDefinedTool request, @@ -148,7 +152,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -174,7 +180,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -214,7 +222,7 @@ private WithRawResponseTask ListToolVersionsInterna ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListToolVersionsInternalAsyncCore( string id, @@ -254,7 +262,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize( @@ -282,7 +292,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -305,7 +317,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > CreateToolVersionAsyncCore( string id, @@ -340,7 +352,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -366,7 +380,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -389,7 +405,9 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task> UpdateToolNameAsyncCore( + private async global::System.Threading.Tasks.Task< + WithRawResponse + > UpdateToolNameAsyncCore( string id, PostedUserDefinedToolName request, RequestOptions? options = null, @@ -422,7 +440,9 @@ private async System.Threading.Tasks.Task> UpdateToolNam .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); return new WithRawResponse() { Data = responseBody, @@ -435,7 +455,9 @@ private async System.Threading.Tasks.Task> UpdateToolNam }; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -458,7 +480,7 @@ private async System.Threading.Tasks.Task> UpdateToolNam } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > GetToolVersionAsyncCore( string id, @@ -492,7 +514,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -518,7 +542,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -541,7 +567,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > UpdateToolDescriptionAsyncCore( string id, @@ -578,7 +604,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -604,7 +632,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -637,7 +667,7 @@ private async System.Threading.Tasks.Task< /// new ToolsListToolsRequest { PageNumber = 0, PageSize = 2 } /// ); /// - public async System.Threading.Tasks.Task> ListToolsAsync( + public async global::System.Threading.Tasks.Task> ListToolsAsync( ToolsListToolsRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -656,7 +686,8 @@ public async System.Threading.Tasks.Task> ListTools request, options, async (request, options, cancellationToken) => - await ListToolsInternalAsync(request, options, cancellationToken), + await ListToolsInternalAsync(request, options, cancellationToken) + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { @@ -712,7 +743,9 @@ await ListToolsInternalAsync(request, options, cancellationToken), /// new ToolsListToolVersionsRequest() /// ); /// - public async System.Threading.Tasks.Task> ListToolVersionsAsync( + public async global::System.Threading.Tasks.Task< + Pager + > ListToolVersionsAsync( string id, ToolsListToolVersionsRequest request, RequestOptions? options = null, @@ -733,7 +766,7 @@ public async System.Threading.Tasks.Task> ListToolV options, async (request, options, cancellationToken) => await ListToolVersionsInternalAsync(id, request, options, cancellationToken) - .ConfigureAwait(false), + .WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { @@ -787,7 +820,7 @@ await ListToolVersionsInternalAsync(id, request, options, cancellationToken) /// /// await client.EmpathicVoice.Tools.DeleteToolAsync("your-tool-id"); /// - public async System.Threading.Tasks.Task DeleteToolAsync( + public async global::System.Threading.Tasks.Task DeleteToolAsync( string id, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -820,7 +853,9 @@ public async System.Threading.Tasks.Task DeleteToolAsync( return; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -894,7 +929,7 @@ public WithRawResponseTask UpdateToolNameAsync( /// /// await client.EmpathicVoice.Tools.DeleteToolVersionAsync("", 1); /// - public async System.Threading.Tasks.Task DeleteToolVersionAsync( + public async global::System.Threading.Tasks.Task DeleteToolVersionAsync( string id, int version, RequestOptions? options = null, @@ -929,7 +964,9 @@ public async System.Threading.Tasks.Task DeleteToolVersionAsync( return; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) diff --git a/src/Hume/EmpathicVoice/Types/AssistantEnd.cs b/src/Hume/EmpathicVoice/Types/AssistantEnd.cs index 2dbcbd5..c1368e3 100644 --- a/src/Hume/EmpathicVoice/Types/AssistantEnd.cs +++ b/src/Hume/EmpathicVoice/Types/AssistantEnd.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -26,16 +26,15 @@ public record AssistantEnd : IJsonOnDeserialized /// /// This message indicates the conclusion of the assistant's response, signaling that the assistant has finished speaking for the current conversational turn. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "assistant_end"; - set => - value.Assert( - value == "assistant_end", - string.Format("'Type' must be {0}", "assistant_end") - ); - } + public AssistantEnd.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -48,4 +47,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "assistant_end"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/AssistantInput.cs b/src/Hume/EmpathicVoice/Types/AssistantInput.cs index e5ba592..fa51bfa 100644 --- a/src/Hume/EmpathicVoice/Types/AssistantInput.cs +++ b/src/Hume/EmpathicVoice/Types/AssistantInput.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -31,16 +31,15 @@ public record AssistantInput : IJsonOnDeserialized /// /// The type of message sent through the socket; must be `assistant_input` for our server to correctly identify and process it as an Assistant Input message. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "assistant_input"; - set => - value.Assert( - value == "assistant_input", - string.Format("'Type' must be {0}", "assistant_input") - ); - } + public AssistantInput.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -53,4 +52,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "assistant_input"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/AssistantMessage.cs b/src/Hume/EmpathicVoice/Types/AssistantMessage.cs index cd93ef7..db25ef9 100644 --- a/src/Hume/EmpathicVoice/Types/AssistantMessage.cs +++ b/src/Hume/EmpathicVoice/Types/AssistantMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -62,16 +62,15 @@ public record AssistantMessage : IJsonOnDeserialized /// /// This message contains both a transcript of the assistant's response and the expression measurement predictions of the assistant's audio output. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "assistant_message"; - set => - value.Assert( - value == "assistant_message", - string.Format("'Type' must be {0}", "assistant_message") - ); - } + public AssistantMessage.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -84,4 +83,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "assistant_message"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/AssistantProsody.cs b/src/Hume/EmpathicVoice/Types/AssistantProsody.cs index e9ed34f..70db628 100644 --- a/src/Hume/EmpathicVoice/Types/AssistantProsody.cs +++ b/src/Hume/EmpathicVoice/Types/AssistantProsody.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -38,16 +38,15 @@ public record AssistantProsody : IJsonOnDeserialized /// /// This message the expression measurement predictions of the assistant's audio output. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "assistant_prosody"; - set => - value.Assert( - value == "assistant_prosody", - string.Format("'Type' must be {0}", "assistant_prosody") - ); - } + public AssistantProsody.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -60,4 +59,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "assistant_prosody"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/AudioConfiguration.cs b/src/Hume/EmpathicVoice/Types/AudioConfiguration.cs index e772e0f..1107044 100644 --- a/src/Hume/EmpathicVoice/Types/AudioConfiguration.cs +++ b/src/Hume/EmpathicVoice/Types/AudioConfiguration.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -27,13 +27,15 @@ public record AudioConfiguration : IJsonOnDeserialized /// /// Encoding format of the audio input, such as `linear16`. /// + [JsonRequired] [JsonPropertyName("encoding")] - public string Encoding - { - get => "linear16"; - set => - value.Assert(value == "linear16", string.Format("'Encoding' must be {0}", "linear16")); - } + public Encoding Encoding { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); /// /// Audio sample rate. Number of samples per second in the audio input, measured in Hertz. diff --git a/src/Hume/EmpathicVoice/Types/AudioInput.cs b/src/Hume/EmpathicVoice/Types/AudioInput.cs index b00f3da..2c1dd9e 100644 --- a/src/Hume/EmpathicVoice/Types/AudioInput.cs +++ b/src/Hume/EmpathicVoice/Types/AudioInput.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -40,16 +40,15 @@ public record AudioInput : IJsonOnDeserialized /// /// This message is used for sending audio input data to EVI for processing and expression measurement. Audio data should be sent as a continuous stream, encoded in Base64. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "audio_input"; - set => - value.Assert( - value == "audio_input", - string.Format("'Type' must be {0}", "audio_input") - ); - } + public AudioInput.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -62,4 +61,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "audio_input"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/AudioOutput.cs b/src/Hume/EmpathicVoice/Types/AudioOutput.cs index 351722c..7a359fc 100644 --- a/src/Hume/EmpathicVoice/Types/AudioOutput.cs +++ b/src/Hume/EmpathicVoice/Types/AudioOutput.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -41,16 +41,15 @@ public record AudioOutput : IJsonOnDeserialized [JsonPropertyName("index")] public required int Index { get; set; } + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "audio_output"; - set => - value.Assert( - value == "audio_output", - string.Format("'Type' must be {0}", "audio_output") - ); - } + public AudioOutput.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -63,4 +62,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "audio_output"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/BuiltInTool.cs b/src/Hume/EmpathicVoice/Types/BuiltInTool.cs index 7227389..581c900 100644 --- a/src/Hume/EmpathicVoice/Types/BuiltInTool.cs +++ b/src/Hume/EmpathicVoice/Types/BuiltInTool.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(BuiltInTool.BuiltInToolSerializer))] [Serializable] public readonly record struct BuiltInTool : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator BuiltInTool(string value) => new(value); + internal class BuiltInToolSerializer : JsonConverter + { + public override BuiltInTool Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new BuiltInTool(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + BuiltInTool value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override BuiltInTool ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new BuiltInTool(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + BuiltInTool value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/BuiltinToolConfig.cs b/src/Hume/EmpathicVoice/Types/BuiltinToolConfig.cs index 3e6365b..66652d7 100644 --- a/src/Hume/EmpathicVoice/Types/BuiltinToolConfig.cs +++ b/src/Hume/EmpathicVoice/Types/BuiltinToolConfig.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ChatMessage.cs b/src/Hume/EmpathicVoice/Types/ChatMessage.cs index c463bd4..e329de9 100644 --- a/src/Hume/EmpathicVoice/Types/ChatMessage.cs +++ b/src/Hume/EmpathicVoice/Types/ChatMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/EmpathicVoice/Types/ChatMetadata.cs b/src/Hume/EmpathicVoice/Types/ChatMetadata.cs index 7c02a6f..2eaad8f 100644 --- a/src/Hume/EmpathicVoice/Types/ChatMetadata.cs +++ b/src/Hume/EmpathicVoice/Types/ChatMetadata.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -50,16 +50,15 @@ public record ChatMetadata : IJsonOnDeserialized /// /// The Chat Metadata message is the first message you receive after establishing a connection with EVI and contains important identifiers for the current Chat session. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "chat_metadata"; - set => - value.Assert( - value == "chat_metadata", - string.Format("'Type' must be {0}", "chat_metadata") - ); - } + public ChatMetadata.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -72,4 +71,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "chat_metadata"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/ConnectSessionSettings.cs b/src/Hume/EmpathicVoice/Types/ConnectSessionSettings.cs index f0a3362..362e905 100644 --- a/src/Hume/EmpathicVoice/Types/ConnectSessionSettings.cs +++ b/src/Hume/EmpathicVoice/Types/ConnectSessionSettings.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/EmpathicVoice/Types/ConnectSessionSettingsAudio.cs b/src/Hume/EmpathicVoice/Types/ConnectSessionSettingsAudio.cs index 87a4e0b..20dae5e 100644 --- a/src/Hume/EmpathicVoice/Types/ConnectSessionSettingsAudio.cs +++ b/src/Hume/EmpathicVoice/Types/ConnectSessionSettingsAudio.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ConnectSessionSettingsContext.cs b/src/Hume/EmpathicVoice/Types/ConnectSessionSettingsContext.cs index bebb139..a887b84 100644 --- a/src/Hume/EmpathicVoice/Types/ConnectSessionSettingsContext.cs +++ b/src/Hume/EmpathicVoice/Types/ConnectSessionSettingsContext.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/Context.cs b/src/Hume/EmpathicVoice/Types/Context.cs index f437ab8..4f2cf6a 100644 --- a/src/Hume/EmpathicVoice/Types/Context.cs +++ b/src/Hume/EmpathicVoice/Types/Context.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ContextType.cs b/src/Hume/EmpathicVoice/Types/ContextType.cs index b9d2f76..b6d220d 100644 --- a/src/Hume/EmpathicVoice/Types/ContextType.cs +++ b/src/Hume/EmpathicVoice/Types/ContextType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ContextType.ContextTypeSerializer))] [Serializable] public readonly record struct ContextType : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator ContextType(string value) => new(value); + internal class ContextTypeSerializer : JsonConverter + { + public override ContextType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ContextType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ContextType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ContextType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ContextType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ContextType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/EmotionScores.cs b/src/Hume/EmpathicVoice/Types/EmotionScores.cs index be8c29a..652065e 100644 --- a/src/Hume/EmpathicVoice/Types/EmotionScores.cs +++ b/src/Hume/EmpathicVoice/Types/EmotionScores.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/Encoding.cs b/src/Hume/EmpathicVoice/Types/Encoding.cs new file mode 100644 index 0000000..871cbbe --- /dev/null +++ b/src/Hume/EmpathicVoice/Types/Encoding.cs @@ -0,0 +1,77 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace Hume.EmpathicVoice; + +[JsonConverter(typeof(EncodingConverter))] +public readonly struct Encoding +{ + public const string Value = "linear16"; + + public static implicit operator string(Encoding _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is Encoding; + + public static bool operator ==(Encoding _, Encoding __) => true; + + public static bool operator !=(Encoding _, Encoding __) => false; + + internal sealed class EncodingConverter : JsonConverter + { + public override Encoding Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != Encoding.Value) + { + throw new JsonException( + "Expected \"" + + Encoding.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new Encoding(); + } + + public override void Write( + Utf8JsonWriter writer, + Encoding value, + JsonSerializerOptions options + ) => writer.WriteStringValue(Encoding.Value); + + public override Encoding ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != Encoding.Value) + { + throw new JsonException( + "Expected \"" + + Encoding.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new Encoding(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Encoding value, + JsonSerializerOptions options + ) => writer.WritePropertyName(Encoding.Value); + } +} diff --git a/src/Hume/EmpathicVoice/Types/ErrorLevel.cs b/src/Hume/EmpathicVoice/Types/ErrorLevel.cs new file mode 100644 index 0000000..51c2f5e --- /dev/null +++ b/src/Hume/EmpathicVoice/Types/ErrorLevel.cs @@ -0,0 +1,77 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace Hume.EmpathicVoice; + +[JsonConverter(typeof(ErrorLevelConverter))] +public readonly struct ErrorLevel +{ + public const string Value = "warn"; + + public static implicit operator string(ErrorLevel _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is ErrorLevel; + + public static bool operator ==(ErrorLevel _, ErrorLevel __) => true; + + public static bool operator !=(ErrorLevel _, ErrorLevel __) => false; + + internal sealed class ErrorLevelConverter : JsonConverter + { + public override ErrorLevel Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != ErrorLevel.Value) + { + throw new JsonException( + "Expected \"" + + ErrorLevel.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new ErrorLevel(); + } + + public override void Write( + Utf8JsonWriter writer, + ErrorLevel value, + JsonSerializerOptions options + ) => writer.WriteStringValue(ErrorLevel.Value); + + public override ErrorLevel ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != ErrorLevel.Value) + { + throw new JsonException( + "Expected \"" + + ErrorLevel.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new ErrorLevel(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ErrorLevel value, + JsonSerializerOptions options + ) => writer.WritePropertyName(ErrorLevel.Value); + } +} diff --git a/src/Hume/EmpathicVoice/Types/ErrorResponse.cs b/src/Hume/EmpathicVoice/Types/ErrorResponse.cs index 2853e46..b890dee 100644 --- a/src/Hume/EmpathicVoice/Types/ErrorResponse.cs +++ b/src/Hume/EmpathicVoice/Types/ErrorResponse.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/HttpValidationError.cs b/src/Hume/EmpathicVoice/Types/HttpValidationError.cs index e7c603e..8a42896 100644 --- a/src/Hume/EmpathicVoice/Types/HttpValidationError.cs +++ b/src/Hume/EmpathicVoice/Types/HttpValidationError.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/Inference.cs b/src/Hume/EmpathicVoice/Types/Inference.cs index c4c6e99..0e06aad 100644 --- a/src/Hume/EmpathicVoice/Types/Inference.cs +++ b/src/Hume/EmpathicVoice/Types/Inference.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/InterruptionSpec.cs b/src/Hume/EmpathicVoice/Types/InterruptionSpec.cs new file mode 100644 index 0000000..2552f51 --- /dev/null +++ b/src/Hume/EmpathicVoice/Types/InterruptionSpec.cs @@ -0,0 +1,29 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using Hume; +using Hume.Core; + +namespace Hume.EmpathicVoice; + +[Serializable] +public record InterruptionSpec : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("min_interruption_ms")] + public int? MinInterruptionMs { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Hume/EmpathicVoice/Types/LanguageModelType.cs b/src/Hume/EmpathicVoice/Types/LanguageModelType.cs index d287523..4979155 100644 --- a/src/Hume/EmpathicVoice/Types/LanguageModelType.cs +++ b/src/Hume/EmpathicVoice/Types/LanguageModelType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(LanguageModelType.LanguageModelTypeSerializer))] [Serializable] public readonly record struct LanguageModelType : IStringEnum { @@ -224,6 +225,55 @@ public override string ToString() public static explicit operator LanguageModelType(string value) => new(value); + internal class LanguageModelTypeSerializer : JsonConverter + { + public override LanguageModelType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new LanguageModelType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + LanguageModelType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override LanguageModelType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new LanguageModelType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + LanguageModelType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/MillisecondInterval.cs b/src/Hume/EmpathicVoice/Types/MillisecondInterval.cs index d5a1a66..5ecafc0 100644 --- a/src/Hume/EmpathicVoice/Types/MillisecondInterval.cs +++ b/src/Hume/EmpathicVoice/Types/MillisecondInterval.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ModelProviderEnum.cs b/src/Hume/EmpathicVoice/Types/ModelProviderEnum.cs index 89b1344..345e417 100644 --- a/src/Hume/EmpathicVoice/Types/ModelProviderEnum.cs +++ b/src/Hume/EmpathicVoice/Types/ModelProviderEnum.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ModelProviderEnum.ModelProviderEnumSerializer))] [Serializable] public readonly record struct ModelProviderEnum : IStringEnum { @@ -72,6 +73,55 @@ public override string ToString() public static explicit operator ModelProviderEnum(string value) => new(value); + internal class ModelProviderEnumSerializer : JsonConverter + { + public override ModelProviderEnum Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ModelProviderEnum(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ModelProviderEnum value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ModelProviderEnum ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ModelProviderEnum(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ModelProviderEnum value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/PauseAssistantMessage.cs b/src/Hume/EmpathicVoice/Types/PauseAssistantMessage.cs index 84c46b4..4065bbc 100644 --- a/src/Hume/EmpathicVoice/Types/PauseAssistantMessage.cs +++ b/src/Hume/EmpathicVoice/Types/PauseAssistantMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -28,16 +28,15 @@ public record PauseAssistantMessage : IJsonOnDeserialized /// /// Once this message is sent, EVI will not respond until a [Resume Assistant message](/reference/empathic-voice-interface-evi/chat/chat#send.Resume%20Assistant%20Message.type) is sent. When paused, EVI won't respond, but transcriptions of your audio inputs will still be recorded. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "pause_assistant_message"; - set => - value.Assert( - value == "pause_assistant_message", - string.Format("'Type' must be {0}", "pause_assistant_message") - ); - } + public PauseAssistantMessage.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -50,4 +49,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "pause_assistant_message"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/PostedBuiltinTool.cs b/src/Hume/EmpathicVoice/Types/PostedBuiltinTool.cs index 7f1e7a3..4b406e7 100644 --- a/src/Hume/EmpathicVoice/Types/PostedBuiltinTool.cs +++ b/src/Hume/EmpathicVoice/Types/PostedBuiltinTool.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedBuiltinToolName.cs b/src/Hume/EmpathicVoice/Types/PostedBuiltinToolName.cs index 057149b..239e2fa 100644 --- a/src/Hume/EmpathicVoice/Types/PostedBuiltinToolName.cs +++ b/src/Hume/EmpathicVoice/Types/PostedBuiltinToolName.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(PostedBuiltinToolName.PostedBuiltinToolNameSerializer))] [Serializable] public readonly record struct PostedBuiltinToolName : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator PostedBuiltinToolName(string value) => new(value); + internal class PostedBuiltinToolNameSerializer : JsonConverter + { + public override PostedBuiltinToolName Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new PostedBuiltinToolName(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + PostedBuiltinToolName value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override PostedBuiltinToolName ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new PostedBuiltinToolName(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + PostedBuiltinToolName value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/PostedConfigPromptSpec.cs b/src/Hume/EmpathicVoice/Types/PostedConfigPromptSpec.cs index a320461..b867f24 100644 --- a/src/Hume/EmpathicVoice/Types/PostedConfigPromptSpec.cs +++ b/src/Hume/EmpathicVoice/Types/PostedConfigPromptSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedEllmModel.cs b/src/Hume/EmpathicVoice/Types/PostedEllmModel.cs index edb97f4..8195646 100644 --- a/src/Hume/EmpathicVoice/Types/PostedEllmModel.cs +++ b/src/Hume/EmpathicVoice/Types/PostedEllmModel.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedEventMessageSpec.cs b/src/Hume/EmpathicVoice/Types/PostedEventMessageSpec.cs index 9a50821..84ce107 100644 --- a/src/Hume/EmpathicVoice/Types/PostedEventMessageSpec.cs +++ b/src/Hume/EmpathicVoice/Types/PostedEventMessageSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedEventMessageSpecs.cs b/src/Hume/EmpathicVoice/Types/PostedEventMessageSpecs.cs index 25d90fd..0aa09ff 100644 --- a/src/Hume/EmpathicVoice/Types/PostedEventMessageSpecs.cs +++ b/src/Hume/EmpathicVoice/Types/PostedEventMessageSpecs.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedInterruptionSpec.cs b/src/Hume/EmpathicVoice/Types/PostedInterruptionSpec.cs new file mode 100644 index 0000000..ff80a83 --- /dev/null +++ b/src/Hume/EmpathicVoice/Types/PostedInterruptionSpec.cs @@ -0,0 +1,35 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using Hume; +using Hume.Core; + +namespace Hume.EmpathicVoice; + +/// +/// An interruption specification posted to the server +/// +[Serializable] +public record PostedInterruptionSpec : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// How long the user must speak while the agent is outputting audio before the agent stops and yields the floor. Lower values make the agent more responsive to the user speaking up, but increase the chance of noise or backchannels stopping the agent unnecessarily. Higher values make the agent harder to interrupt, allowing it to finish more of its response before yielding. Accepts values between 50 and 2000 milliseconds. + /// + [JsonPropertyName("min_interruption_ms")] + public int? MinInterruptionMs { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Hume/EmpathicVoice/Types/PostedLanguageModel.cs b/src/Hume/EmpathicVoice/Types/PostedLanguageModel.cs index 85987a9..f89dd57 100644 --- a/src/Hume/EmpathicVoice/Types/PostedLanguageModel.cs +++ b/src/Hume/EmpathicVoice/Types/PostedLanguageModel.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedNudgeSpec.cs b/src/Hume/EmpathicVoice/Types/PostedNudgeSpec.cs index 4e7b6cd..02e732d 100644 --- a/src/Hume/EmpathicVoice/Types/PostedNudgeSpec.cs +++ b/src/Hume/EmpathicVoice/Types/PostedNudgeSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedTimeoutSpec.cs b/src/Hume/EmpathicVoice/Types/PostedTimeoutSpec.cs index d5a9888..9195d33 100644 --- a/src/Hume/EmpathicVoice/Types/PostedTimeoutSpec.cs +++ b/src/Hume/EmpathicVoice/Types/PostedTimeoutSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecs.cs b/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecs.cs index 8884ed4..1557845 100644 --- a/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecs.cs +++ b/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecs.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecsInactivity.cs b/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecsInactivity.cs index 566dd06..5d959ab 100644 --- a/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecsInactivity.cs +++ b/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecsInactivity.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecsMaxDuration.cs b/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecsMaxDuration.cs index b10e37b..f7505ef 100644 --- a/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecsMaxDuration.cs +++ b/src/Hume/EmpathicVoice/Types/PostedTimeoutSpecsMaxDuration.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedTurnDetectionSpec.cs b/src/Hume/EmpathicVoice/Types/PostedTurnDetectionSpec.cs new file mode 100644 index 0000000..69e4203 --- /dev/null +++ b/src/Hume/EmpathicVoice/Types/PostedTurnDetectionSpec.cs @@ -0,0 +1,47 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using Hume; +using Hume.Core; + +namespace Hume.EmpathicVoice; + +/// +/// A turn detection specification posted to the server +/// +[Serializable] +public record PostedTurnDetectionSpec : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// How long the user must be silent before EVI considers their turn complete and begins generating a response. Lower values make conversations feel snappier but increase the chance of the agent responding during a mid-thought pause. Higher values give users more room to pause and collect their thoughts before the agent responds. Accepts values between 500 and 3000 milliseconds. + /// + [JsonPropertyName("end_of_turn_silence_ms")] + public int? EndOfTurnSilenceMs { get; set; } + + /// + /// The duration of audio captured before the detected start of speech. This ensures the beginning of an utterance is not clipped. Accepts values between 0 and 1000 milliseconds. + /// + [JsonPropertyName("prefix_padding_ms")] + public int? PrefixPaddingMs { get; set; } + + /// + /// How confident the system must be that audio contains speech before it begins processing. Lower values increase sensitivity, capturing softer speech at the cost of more noise-triggered processing. Higher values require clearer, louder audio to register as speech. Accepts values between 0.0 and 1.0. + /// + [JsonPropertyName("speech_detection_threshold")] + public float? SpeechDetectionThreshold { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Hume/EmpathicVoice/Types/PostedUserDefinedToolSpec.cs b/src/Hume/EmpathicVoice/Types/PostedUserDefinedToolSpec.cs index fa71c3b..0fbce2f 100644 --- a/src/Hume/EmpathicVoice/Types/PostedUserDefinedToolSpec.cs +++ b/src/Hume/EmpathicVoice/Types/PostedUserDefinedToolSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PostedWebhookEventType.cs b/src/Hume/EmpathicVoice/Types/PostedWebhookEventType.cs index 0a28447..1496c9c 100644 --- a/src/Hume/EmpathicVoice/Types/PostedWebhookEventType.cs +++ b/src/Hume/EmpathicVoice/Types/PostedWebhookEventType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(PostedWebhookEventType.PostedWebhookEventTypeSerializer))] [Serializable] public readonly record struct PostedWebhookEventType : IStringEnum { @@ -54,6 +55,55 @@ public override string ToString() public static explicit operator PostedWebhookEventType(string value) => new(value); + internal class PostedWebhookEventTypeSerializer : JsonConverter + { + public override PostedWebhookEventType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new PostedWebhookEventType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + PostedWebhookEventType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override PostedWebhookEventType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new PostedWebhookEventType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + PostedWebhookEventType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/PostedWebhookSpec.cs b/src/Hume/EmpathicVoice/Types/PostedWebhookSpec.cs index 569a252..d63080a 100644 --- a/src/Hume/EmpathicVoice/Types/PostedWebhookSpec.cs +++ b/src/Hume/EmpathicVoice/Types/PostedWebhookSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/PromptExpansionSpec.cs b/src/Hume/EmpathicVoice/Types/PromptExpansionSpec.cs index eef2e28..853f4ed 100644 --- a/src/Hume/EmpathicVoice/Types/PromptExpansionSpec.cs +++ b/src/Hume/EmpathicVoice/Types/PromptExpansionSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ProsodyInference.cs b/src/Hume/EmpathicVoice/Types/ProsodyInference.cs index 37d5dbe..cfe781a 100644 --- a/src/Hume/EmpathicVoice/Types/ProsodyInference.cs +++ b/src/Hume/EmpathicVoice/Types/ProsodyInference.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ResumeAssistantMessage.cs b/src/Hume/EmpathicVoice/Types/ResumeAssistantMessage.cs index b4aaff8..4bface2 100644 --- a/src/Hume/EmpathicVoice/Types/ResumeAssistantMessage.cs +++ b/src/Hume/EmpathicVoice/Types/ResumeAssistantMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -28,16 +28,15 @@ public record ResumeAssistantMessage : IJsonOnDeserialized /// /// Upon resuming, if any audio input was sent during the pause, EVI will retain context from all messages sent but only respond to the last user message. (e.g., If you ask EVI two questions while paused and then send a `resume_assistant_message`, EVI will respond to the second question and have added the first question to its conversation context.) /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "resume_assistant_message"; - set => - value.Assert( - value == "resume_assistant_message", - string.Format("'Type' must be {0}", "resume_assistant_message") - ); - } + public ResumeAssistantMessage.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -50,4 +49,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "resume_assistant_message"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/ReturnBuiltinTool.cs b/src/Hume/EmpathicVoice/Types/ReturnBuiltinTool.cs index 82d23be..a0e5e1c 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnBuiltinTool.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnBuiltinTool.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnBuiltinToolToolType.cs b/src/Hume/EmpathicVoice/Types/ReturnBuiltinToolToolType.cs index 7679a50..3a5d971 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnBuiltinToolToolType.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnBuiltinToolToolType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnBuiltinToolToolType.ReturnBuiltinToolToolTypeSerializer))] [Serializable] public readonly record struct ReturnBuiltinToolToolType : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator ReturnBuiltinToolToolType(string value) => new(value); + internal class ReturnBuiltinToolToolTypeSerializer : JsonConverter + { + public override ReturnBuiltinToolToolType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnBuiltinToolToolType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnBuiltinToolToolType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnBuiltinToolToolType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnBuiltinToolToolType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnBuiltinToolToolType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChat.cs b/src/Hume/EmpathicVoice/Types/ReturnChat.cs index a72d068..7f28050 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChat.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChat.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatAudioReconstruction.cs b/src/Hume/EmpathicVoice/Types/ReturnChatAudioReconstruction.cs index 15b086e..22c9b5a 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatAudioReconstruction.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatAudioReconstruction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatAudioReconstructionStatus.cs b/src/Hume/EmpathicVoice/Types/ReturnChatAudioReconstructionStatus.cs index 77a85b3..10c6f7a 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatAudioReconstructionStatus.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatAudioReconstructionStatus.cs @@ -1,9 +1,12 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(ReturnChatAudioReconstructionStatus.ReturnChatAudioReconstructionStatusSerializer) +)] [Serializable] public readonly record struct ReturnChatAudioReconstructionStatus : IStringEnum { @@ -59,6 +62,56 @@ public static explicit operator string(ReturnChatAudioReconstructionStatus value public static explicit operator ReturnChatAudioReconstructionStatus(string value) => new(value); + internal class ReturnChatAudioReconstructionStatusSerializer + : JsonConverter + { + public override ReturnChatAudioReconstructionStatus Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatAudioReconstructionStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatAudioReconstructionStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatAudioReconstructionStatus ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatAudioReconstructionStatus(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatAudioReconstructionStatus value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatEvent.cs b/src/Hume/EmpathicVoice/Types/ReturnChatEvent.cs index 32e0b18..e5a30bc 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatEvent.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatEvent.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatEventRole.cs b/src/Hume/EmpathicVoice/Types/ReturnChatEventRole.cs index 89543c8..e10bcc0 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatEventRole.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatEventRole.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnChatEventRole.ReturnChatEventRoleSerializer))] [Serializable] public readonly record struct ReturnChatEventRole : IStringEnum { @@ -56,6 +57,55 @@ public override string ToString() public static explicit operator ReturnChatEventRole(string value) => new(value); + internal class ReturnChatEventRoleSerializer : JsonConverter + { + public override ReturnChatEventRole Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatEventRole(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatEventRole value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatEventRole ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatEventRole(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatEventRole value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatEventType.cs b/src/Hume/EmpathicVoice/Types/ReturnChatEventType.cs index bde331c..839b6fe 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatEventType.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatEventType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnChatEventType.ReturnChatEventTypeSerializer))] [Serializable] public readonly record struct ReturnChatEventType : IStringEnum { @@ -78,6 +79,55 @@ public override string ToString() public static explicit operator ReturnChatEventType(string value) => new(value); + internal class ReturnChatEventTypeSerializer : JsonConverter + { + public override ReturnChatEventType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatEventType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatEventType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatEventType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatEventType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatEventType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatGroup.cs b/src/Hume/EmpathicVoice/Types/ReturnChatGroup.cs index 2e72af6..3a728dc 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatGroup.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatGroup.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedAudioReconstructions.cs b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedAudioReconstructions.cs index dd64f2c..2298931 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedAudioReconstructions.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedAudioReconstructions.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedAudioReconstructionsPaginationDirection.cs b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedAudioReconstructionsPaginationDirection.cs index 53e594e..d53ba10 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedAudioReconstructionsPaginationDirection.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedAudioReconstructionsPaginationDirection.cs @@ -1,10 +1,11 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; [JsonConverter( - typeof(StringEnumSerializer) + typeof(ReturnChatGroupPagedAudioReconstructionsPaginationDirection.ReturnChatGroupPagedAudioReconstructionsPaginationDirectionSerializer) )] [Serializable] public readonly record struct ReturnChatGroupPagedAudioReconstructionsPaginationDirection @@ -69,6 +70,56 @@ public static explicit operator ReturnChatGroupPagedAudioReconstructionsPaginati string value ) => new(value); + internal class ReturnChatGroupPagedAudioReconstructionsPaginationDirectionSerializer + : JsonConverter + { + public override ReturnChatGroupPagedAudioReconstructionsPaginationDirection Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatGroupPagedAudioReconstructionsPaginationDirection(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatGroupPagedAudioReconstructionsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatGroupPagedAudioReconstructionsPaginationDirection ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatGroupPagedAudioReconstructionsPaginationDirection(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatGroupPagedAudioReconstructionsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedChats.cs b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedChats.cs index 71afe7f..f0ef690 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedChats.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedChats.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedChatsPaginationDirection.cs b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedChatsPaginationDirection.cs index b9c9c35..65adc6e 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedChatsPaginationDirection.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedChatsPaginationDirection.cs @@ -1,9 +1,12 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(ReturnChatGroupPagedChatsPaginationDirection.ReturnChatGroupPagedChatsPaginationDirectionSerializer) +)] [Serializable] public readonly record struct ReturnChatGroupPagedChatsPaginationDirection : IStringEnum { @@ -58,6 +61,56 @@ public static explicit operator string(ReturnChatGroupPagedChatsPaginationDirect public static explicit operator ReturnChatGroupPagedChatsPaginationDirection(string value) => new(value); + internal class ReturnChatGroupPagedChatsPaginationDirectionSerializer + : JsonConverter + { + public override ReturnChatGroupPagedChatsPaginationDirection Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatGroupPagedChatsPaginationDirection(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatGroupPagedChatsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatGroupPagedChatsPaginationDirection ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatGroupPagedChatsPaginationDirection(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatGroupPagedChatsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedEvents.cs b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedEvents.cs index afb9c8b..5d212d1 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedEvents.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedEvents.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedEventsPaginationDirection.cs b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedEventsPaginationDirection.cs index 6cec5b5..39aad29 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedEventsPaginationDirection.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatGroupPagedEventsPaginationDirection.cs @@ -1,9 +1,12 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(ReturnChatGroupPagedEventsPaginationDirection.ReturnChatGroupPagedEventsPaginationDirectionSerializer) +)] [Serializable] public readonly record struct ReturnChatGroupPagedEventsPaginationDirection : IStringEnum { @@ -58,6 +61,56 @@ public static explicit operator string(ReturnChatGroupPagedEventsPaginationDirec public static explicit operator ReturnChatGroupPagedEventsPaginationDirection(string value) => new(value); + internal class ReturnChatGroupPagedEventsPaginationDirectionSerializer + : JsonConverter + { + public override ReturnChatGroupPagedEventsPaginationDirection Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatGroupPagedEventsPaginationDirection(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatGroupPagedEventsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatGroupPagedEventsPaginationDirection ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatGroupPagedEventsPaginationDirection(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatGroupPagedEventsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatPagedEvents.cs b/src/Hume/EmpathicVoice/Types/ReturnChatPagedEvents.cs index 093bbca..552b996 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatPagedEvents.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatPagedEvents.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatPagedEventsPaginationDirection.cs b/src/Hume/EmpathicVoice/Types/ReturnChatPagedEventsPaginationDirection.cs index dfb6921..05dbe01 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatPagedEventsPaginationDirection.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatPagedEventsPaginationDirection.cs @@ -1,9 +1,12 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(ReturnChatPagedEventsPaginationDirection.ReturnChatPagedEventsPaginationDirectionSerializer) +)] [Serializable] public readonly record struct ReturnChatPagedEventsPaginationDirection : IStringEnum { @@ -58,6 +61,56 @@ public static explicit operator string(ReturnChatPagedEventsPaginationDirection public static explicit operator ReturnChatPagedEventsPaginationDirection(string value) => new(value); + internal class ReturnChatPagedEventsPaginationDirectionSerializer + : JsonConverter + { + public override ReturnChatPagedEventsPaginationDirection Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatPagedEventsPaginationDirection(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatPagedEventsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatPagedEventsPaginationDirection ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatPagedEventsPaginationDirection(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatPagedEventsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatPagedEventsStatus.cs b/src/Hume/EmpathicVoice/Types/ReturnChatPagedEventsStatus.cs index 725f375..9c47cc2 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatPagedEventsStatus.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatPagedEventsStatus.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnChatPagedEventsStatus.ReturnChatPagedEventsStatusSerializer))] [Serializable] public readonly record struct ReturnChatPagedEventsStatus : IStringEnum { @@ -64,6 +65,56 @@ public override string ToString() public static explicit operator ReturnChatPagedEventsStatus(string value) => new(value); + internal class ReturnChatPagedEventsStatusSerializer + : JsonConverter + { + public override ReturnChatPagedEventsStatus Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatPagedEventsStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatPagedEventsStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatPagedEventsStatus ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatPagedEventsStatus(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatPagedEventsStatus value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnChatStatus.cs b/src/Hume/EmpathicVoice/Types/ReturnChatStatus.cs index fd3dba0..9185c2e 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnChatStatus.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnChatStatus.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnChatStatus.ReturnChatStatusSerializer))] [Serializable] public readonly record struct ReturnChatStatus : IStringEnum { @@ -60,6 +61,55 @@ public override string ToString() public static explicit operator ReturnChatStatus(string value) => new(value); + internal class ReturnChatStatusSerializer : JsonConverter + { + public override ReturnChatStatus Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnChatStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnChatStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnChatStatus ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnChatStatus(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnChatStatus value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnConfig.cs b/src/Hume/EmpathicVoice/Types/ReturnConfig.cs index 0579243..b7db48d 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnConfig.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnConfig.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -47,6 +47,9 @@ public record ReturnConfig : IJsonOnDeserialized [JsonPropertyName("id")] public string? Id { get; set; } + [JsonPropertyName("interruption")] + public ReturnInterruptionSpec? Interruption { get; set; } + [JsonPropertyName("language_model")] public ReturnLanguageModel? LanguageModel { get; set; } @@ -77,6 +80,9 @@ public record ReturnConfig : IJsonOnDeserialized [JsonPropertyName("tools")] public IEnumerable? Tools { get; set; } + [JsonPropertyName("turn_detection")] + public ReturnTurnDetectionSpec? TurnDetection { get; set; } + /// /// Version number for a Config. /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnConfigSpec.cs b/src/Hume/EmpathicVoice/Types/ReturnConfigSpec.cs index 12e9bba..52642d8 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnConfigSpec.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnConfigSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnEllmModel.cs b/src/Hume/EmpathicVoice/Types/ReturnEllmModel.cs index 88da003..8aa444e 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnEllmModel.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnEllmModel.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnEventMessageSpec.cs b/src/Hume/EmpathicVoice/Types/ReturnEventMessageSpec.cs index bc68bc3..083de66 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnEventMessageSpec.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnEventMessageSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnEventMessageSpecs.cs b/src/Hume/EmpathicVoice/Types/ReturnEventMessageSpecs.cs index cee60bc..bea2cf7 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnEventMessageSpecs.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnEventMessageSpecs.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnInterruptionSpec.cs b/src/Hume/EmpathicVoice/Types/ReturnInterruptionSpec.cs new file mode 100644 index 0000000..c32de05 --- /dev/null +++ b/src/Hume/EmpathicVoice/Types/ReturnInterruptionSpec.cs @@ -0,0 +1,35 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using Hume; +using Hume.Core; + +namespace Hume.EmpathicVoice; + +/// +/// An interruption configuration returned from the server +/// +[Serializable] +public record ReturnInterruptionSpec : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// How long the user must speak while the agent is outputting audio before the agent stops and yields the floor. Lower values make the agent more responsive to the user speaking up, but increase the chance of noise or backchannels stopping the agent unnecessarily. Higher values make the agent harder to interrupt, allowing it to finish more of its response before yielding. Accepts values between 50 and 2000 milliseconds. + /// + [JsonPropertyName("min_interruption_ms")] + public int? MinInterruptionMs { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Hume/EmpathicVoice/Types/ReturnLanguageModel.cs b/src/Hume/EmpathicVoice/Types/ReturnLanguageModel.cs index c4880a2..8b91fb7 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnLanguageModel.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnLanguageModel.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnNudgeSpec.cs b/src/Hume/EmpathicVoice/Types/ReturnNudgeSpec.cs index a9ff32e..4996c8d 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnNudgeSpec.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnNudgeSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnPagedChatGroups.cs b/src/Hume/EmpathicVoice/Types/ReturnPagedChatGroups.cs index 35c9f60..f150953 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPagedChatGroups.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPagedChatGroups.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnPagedChatGroupsPaginationDirection.cs b/src/Hume/EmpathicVoice/Types/ReturnPagedChatGroupsPaginationDirection.cs index fc6e851..486ca87 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPagedChatGroupsPaginationDirection.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPagedChatGroupsPaginationDirection.cs @@ -1,9 +1,12 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(ReturnPagedChatGroupsPaginationDirection.ReturnPagedChatGroupsPaginationDirectionSerializer) +)] [Serializable] public readonly record struct ReturnPagedChatGroupsPaginationDirection : IStringEnum { @@ -58,6 +61,56 @@ public static explicit operator string(ReturnPagedChatGroupsPaginationDirection public static explicit operator ReturnPagedChatGroupsPaginationDirection(string value) => new(value); + internal class ReturnPagedChatGroupsPaginationDirectionSerializer + : JsonConverter + { + public override ReturnPagedChatGroupsPaginationDirection Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnPagedChatGroupsPaginationDirection(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnPagedChatGroupsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnPagedChatGroupsPaginationDirection ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnPagedChatGroupsPaginationDirection(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnPagedChatGroupsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnPagedChats.cs b/src/Hume/EmpathicVoice/Types/ReturnPagedChats.cs index 85928d5..410eb8b 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPagedChats.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPagedChats.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnPagedChatsPaginationDirection.cs b/src/Hume/EmpathicVoice/Types/ReturnPagedChatsPaginationDirection.cs index 3c378da..9d9e1cb 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPagedChatsPaginationDirection.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPagedChatsPaginationDirection.cs @@ -1,9 +1,12 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter( + typeof(ReturnPagedChatsPaginationDirection.ReturnPagedChatsPaginationDirectionSerializer) +)] [Serializable] public readonly record struct ReturnPagedChatsPaginationDirection : IStringEnum { @@ -53,6 +56,56 @@ public static explicit operator string(ReturnPagedChatsPaginationDirection value public static explicit operator ReturnPagedChatsPaginationDirection(string value) => new(value); + internal class ReturnPagedChatsPaginationDirectionSerializer + : JsonConverter + { + public override ReturnPagedChatsPaginationDirection Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnPagedChatsPaginationDirection(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnPagedChatsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnPagedChatsPaginationDirection ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnPagedChatsPaginationDirection(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnPagedChatsPaginationDirection value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnPagedConfigs.cs b/src/Hume/EmpathicVoice/Types/ReturnPagedConfigs.cs index 389c402..7055d68 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPagedConfigs.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPagedConfigs.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnPagedPrompts.cs b/src/Hume/EmpathicVoice/Types/ReturnPagedPrompts.cs index a34c518..b3ed6b6 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPagedPrompts.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPagedPrompts.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnPagedUserDefinedTools.cs b/src/Hume/EmpathicVoice/Types/ReturnPagedUserDefinedTools.cs index de9004e..b20665f 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPagedUserDefinedTools.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPagedUserDefinedTools.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnPrompt.cs b/src/Hume/EmpathicVoice/Types/ReturnPrompt.cs index 12624b1..30e8ea3 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPrompt.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPrompt.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnPromptVersionType.cs b/src/Hume/EmpathicVoice/Types/ReturnPromptVersionType.cs index a39099f..c165bfe 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnPromptVersionType.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnPromptVersionType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnPromptVersionType.ReturnPromptVersionTypeSerializer))] [Serializable] public readonly record struct ReturnPromptVersionType : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator ReturnPromptVersionType(string value) => new(value); + internal class ReturnPromptVersionTypeSerializer : JsonConverter + { + public override ReturnPromptVersionType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnPromptVersionType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnPromptVersionType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnPromptVersionType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnPromptVersionType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnPromptVersionType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnTimeoutSpec.cs b/src/Hume/EmpathicVoice/Types/ReturnTimeoutSpec.cs index 13af8f9..1b1baec 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnTimeoutSpec.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnTimeoutSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnTimeoutSpecs.cs b/src/Hume/EmpathicVoice/Types/ReturnTimeoutSpecs.cs index 6e81a07..03cdf59 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnTimeoutSpecs.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnTimeoutSpecs.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnTurnDetectionSpec.cs b/src/Hume/EmpathicVoice/Types/ReturnTurnDetectionSpec.cs new file mode 100644 index 0000000..0386cfe --- /dev/null +++ b/src/Hume/EmpathicVoice/Types/ReturnTurnDetectionSpec.cs @@ -0,0 +1,47 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using Hume; +using Hume.Core; + +namespace Hume.EmpathicVoice; + +/// +/// A turn detection configuration returned from the server +/// +[Serializable] +public record ReturnTurnDetectionSpec : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + /// + /// How long the user must be silent before EVI considers their turn complete and begins generating a response. Lower values make conversations feel snappier but increase the chance of the agent responding during a mid-thought pause. Higher values give users more room to pause and collect their thoughts before the agent responds. Accepts values between 500 and 3000 milliseconds. + /// + [JsonPropertyName("end_of_turn_silence_ms")] + public int? EndOfTurnSilenceMs { get; set; } + + /// + /// The duration of audio captured before the detected start of speech. This ensures the beginning of an utterance is not clipped. Accepts values between 0 and 1000 milliseconds. + /// + [JsonPropertyName("prefix_padding_ms")] + public int? PrefixPaddingMs { get; set; } + + /// + /// How confident the system must be that audio contains speech before it begins processing. Lower values increase sensitivity, capturing softer speech at the cost of more noise-triggered processing. Higher values require clearer, louder audio to register as speech. Accepts values between 0.0 and 1.0. + /// + [JsonPropertyName("speech_detection_threshold")] + public float? SpeechDetectionThreshold { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Hume/EmpathicVoice/Types/ReturnUserDefinedTool.cs b/src/Hume/EmpathicVoice/Types/ReturnUserDefinedTool.cs index d774b19..72a9e6b 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnUserDefinedTool.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnUserDefinedTool.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnUserDefinedToolToolType.cs b/src/Hume/EmpathicVoice/Types/ReturnUserDefinedToolToolType.cs index 2fd05b7..1d130b0 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnUserDefinedToolToolType.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnUserDefinedToolToolType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnUserDefinedToolToolType.ReturnUserDefinedToolToolTypeSerializer))] [Serializable] public readonly record struct ReturnUserDefinedToolToolType : IStringEnum { @@ -52,6 +53,56 @@ public override string ToString() public static explicit operator ReturnUserDefinedToolToolType(string value) => new(value); + internal class ReturnUserDefinedToolToolTypeSerializer + : JsonConverter + { + public override ReturnUserDefinedToolToolType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnUserDefinedToolToolType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnUserDefinedToolToolType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnUserDefinedToolToolType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnUserDefinedToolToolType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnUserDefinedToolToolType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnUserDefinedToolVersionType.cs b/src/Hume/EmpathicVoice/Types/ReturnUserDefinedToolVersionType.cs index a5c5c55..eeb5274 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnUserDefinedToolVersionType.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnUserDefinedToolVersionType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnUserDefinedToolVersionType.ReturnUserDefinedToolVersionTypeSerializer))] [Serializable] public readonly record struct ReturnUserDefinedToolVersionType : IStringEnum { @@ -52,6 +53,56 @@ public override string ToString() public static explicit operator ReturnUserDefinedToolVersionType(string value) => new(value); + internal class ReturnUserDefinedToolVersionTypeSerializer + : JsonConverter + { + public override ReturnUserDefinedToolVersionType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnUserDefinedToolVersionType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnUserDefinedToolVersionType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnUserDefinedToolVersionType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnUserDefinedToolVersionType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnUserDefinedToolVersionType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnVoice.cs b/src/Hume/EmpathicVoice/Types/ReturnVoice.cs index ddf5b99..209a38b 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnVoice.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnVoice.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ReturnWebhookEventType.cs b/src/Hume/EmpathicVoice/Types/ReturnWebhookEventType.cs index 613d705..f5891e4 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnWebhookEventType.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnWebhookEventType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ReturnWebhookEventType.ReturnWebhookEventTypeSerializer))] [Serializable] public readonly record struct ReturnWebhookEventType : IStringEnum { @@ -54,6 +55,55 @@ public override string ToString() public static explicit operator ReturnWebhookEventType(string value) => new(value); + internal class ReturnWebhookEventTypeSerializer : JsonConverter + { + public override ReturnWebhookEventType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ReturnWebhookEventType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ReturnWebhookEventType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ReturnWebhookEventType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ReturnWebhookEventType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ReturnWebhookEventType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/ReturnWebhookSpec.cs b/src/Hume/EmpathicVoice/Types/ReturnWebhookSpec.cs index ac9c214..666ef2b 100644 --- a/src/Hume/EmpathicVoice/Types/ReturnWebhookSpec.cs +++ b/src/Hume/EmpathicVoice/Types/ReturnWebhookSpec.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/Role.cs b/src/Hume/EmpathicVoice/Types/Role.cs index 34b530a..1b31d42 100644 --- a/src/Hume/EmpathicVoice/Types/Role.cs +++ b/src/Hume/EmpathicVoice/Types/Role.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Role.RoleSerializer))] [Serializable] public readonly record struct Role : IStringEnum { @@ -58,6 +59,51 @@ public override string ToString() public static explicit operator Role(string value) => new(value); + internal class RoleSerializer : JsonConverter + { + public override Role Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Role(stringValue); + } + + public override void Write(Utf8JsonWriter writer, Role value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + + public override Role ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new Role(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Role value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/SessionSettings.cs b/src/Hume/EmpathicVoice/Types/SessionSettings.cs index e650617..46079e2 100644 --- a/src/Hume/EmpathicVoice/Types/SessionSettings.cs +++ b/src/Hume/EmpathicVoice/Types/SessionSettings.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; @@ -94,16 +94,15 @@ public record SessionSettings : IJsonOnDeserialized /// /// For more information, please refer to the [Session Settings section](/docs/empathic-voice-interface-evi/configuration#session-settings) on the EVI Configuration page. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "session_settings"; - set => - value.Assert( - value == "session_settings", - string.Format("'Type' must be {0}", "session_settings") - ); - } + public SessionSettings.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); /// /// This field allows you to assign values to dynamic variables referenced in your system prompt. @@ -132,4 +131,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "session_settings"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/Tool.cs b/src/Hume/EmpathicVoice/Types/Tool.cs index c38cda7..c019be6 100644 --- a/src/Hume/EmpathicVoice/Types/Tool.cs +++ b/src/Hume/EmpathicVoice/Types/Tool.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/ToolCallMessage.cs b/src/Hume/EmpathicVoice/Types/ToolCallMessage.cs index 27024ed..6fddbd4 100644 --- a/src/Hume/EmpathicVoice/Types/ToolCallMessage.cs +++ b/src/Hume/EmpathicVoice/Types/ToolCallMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -62,12 +62,15 @@ public record ToolCallMessage : IJsonOnDeserialized /// /// This message indicates that the supplemental LLM has detected a need to invoke the specified tool. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "tool_call"; - set => value.Assert(value == "tool_call", string.Format("'Type' must be {0}", "tool_call")); - } + public ToolCallMessage.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -80,4 +83,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "tool_call"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/ToolErrorMessage.cs b/src/Hume/EmpathicVoice/Types/ToolErrorMessage.cs index d0dc915..55a0c80 100644 --- a/src/Hume/EmpathicVoice/Types/ToolErrorMessage.cs +++ b/src/Hume/EmpathicVoice/Types/ToolErrorMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -66,13 +66,15 @@ public record ToolErrorMessage : IJsonOnDeserialized /// /// Upon receiving a [Tool Call message](/reference/empathic-voice-interface-evi/chat/chat#receive.Tool%20Call%20Message.type) and failing to invoke the function, this message is sent to notify EVI of the tool's failure. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "tool_error"; - set => - value.Assert(value == "tool_error", string.Format("'Type' must be {0}", "tool_error")); - } + public ToolErrorMessage.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -85,4 +87,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "tool_error"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/ToolResponseMessage.cs b/src/Hume/EmpathicVoice/Types/ToolResponseMessage.cs index c71aa19..ddeff85 100644 --- a/src/Hume/EmpathicVoice/Types/ToolResponseMessage.cs +++ b/src/Hume/EmpathicVoice/Types/ToolResponseMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -48,16 +48,15 @@ public record ToolResponseMessage : IJsonOnDeserialized /// /// Upon receiving a [Tool Call message](/reference/empathic-voice-interface-evi/chat/chat#receive.Tool%20Call%20Message.type) and successfully invoking the function, this message is sent to convey the result of the function call back to EVI. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "tool_response"; - set => - value.Assert( - value == "tool_response", - string.Format("'Type' must be {0}", "tool_response") - ); - } + public ToolResponseMessage.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -70,4 +69,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "tool_response"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/ToolType.cs b/src/Hume/EmpathicVoice/Types/ToolType.cs index de330c4..a43bf8c 100644 --- a/src/Hume/EmpathicVoice/Types/ToolType.cs +++ b/src/Hume/EmpathicVoice/Types/ToolType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(ToolType.ToolTypeSerializer))] [Serializable] public readonly record struct ToolType : IStringEnum { @@ -50,6 +51,55 @@ public override string ToString() public static explicit operator ToolType(string value) => new(value); + internal class ToolTypeSerializer : JsonConverter + { + public override ToolType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new ToolType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + ToolType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override ToolType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new ToolType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + ToolType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/TurnDetectionSpec.cs b/src/Hume/EmpathicVoice/Types/TurnDetectionSpec.cs new file mode 100644 index 0000000..0429ac9 --- /dev/null +++ b/src/Hume/EmpathicVoice/Types/TurnDetectionSpec.cs @@ -0,0 +1,35 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using Hume; +using Hume.Core; + +namespace Hume.EmpathicVoice; + +[Serializable] +public record TurnDetectionSpec : IJsonOnDeserialized +{ + [JsonExtensionData] + private readonly IDictionary _extensionData = + new Dictionary(); + + [JsonPropertyName("end_of_turn_silence_ms")] + public int? EndOfTurnSilenceMs { get; set; } + + [JsonPropertyName("prefix_padding_ms")] + public int? PrefixPaddingMs { get; set; } + + [JsonPropertyName("speech_detection_threshold")] + public double? SpeechDetectionThreshold { get; set; } + + [JsonIgnore] + public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); + + void IJsonOnDeserialized.OnDeserialized() => + AdditionalProperties.CopyFromExtensionData(_extensionData); + + /// + public override string ToString() + { + return JsonUtils.Serialize(this); + } +} diff --git a/src/Hume/EmpathicVoice/Types/UserInput.cs b/src/Hume/EmpathicVoice/Types/UserInput.cs index dde5e3a..1990b49 100644 --- a/src/Hume/EmpathicVoice/Types/UserInput.cs +++ b/src/Hume/EmpathicVoice/Types/UserInput.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -34,13 +34,15 @@ public record UserInput : IJsonOnDeserialized /// /// The type of message sent through the socket; must be `user_input` for our server to correctly identify and process it as a User Input message. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "user_input"; - set => - value.Assert(value == "user_input", string.Format("'Type' must be {0}", "user_input")); - } + public UserInput.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -53,4 +55,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "user_input"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/UserInterruption.cs b/src/Hume/EmpathicVoice/Types/UserInterruption.cs index 4ad01f3..98d7cb1 100644 --- a/src/Hume/EmpathicVoice/Types/UserInterruption.cs +++ b/src/Hume/EmpathicVoice/Types/UserInterruption.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -34,16 +34,15 @@ public record UserInterruption : IJsonOnDeserialized /// /// This message indicates the user has interrupted the assistant's response. EVI detects the interruption in real-time and sends this message to signal the interruption event. This message allows the system to stop the current audio playback, clear the audio queue, and prepare to handle new user input. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "user_interruption"; - set => - value.Assert( - value == "user_interruption", - string.Format("'Type' must be {0}", "user_interruption") - ); - } + public UserInterruption.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -56,4 +55,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "user_interruption"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/UserMessage.cs b/src/Hume/EmpathicVoice/Types/UserMessage.cs index 3ba6135..f8aff72 100644 --- a/src/Hume/EmpathicVoice/Types/UserMessage.cs +++ b/src/Hume/EmpathicVoice/Types/UserMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -59,16 +59,15 @@ public record UserMessage : IJsonOnDeserialized [JsonPropertyName("time")] public required MillisecondInterval Time { get; set; } + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "user_message"; - set => - value.Assert( - value == "user_message", - string.Format("'Type' must be {0}", "user_message") - ); - } + public UserMessage.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -81,4 +80,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "user_message"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/ValidationError.cs b/src/Hume/EmpathicVoice/Types/ValidationError.cs index cd78030..74f6483 100644 --- a/src/Hume/EmpathicVoice/Types/ValidationError.cs +++ b/src/Hume/EmpathicVoice/Types/ValidationError.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/EmpathicVoice/Types/VoiceId.cs b/src/Hume/EmpathicVoice/Types/VoiceId.cs index e0a71bd..dc3bd0e 100644 --- a/src/Hume/EmpathicVoice/Types/VoiceId.cs +++ b/src/Hume/EmpathicVoice/Types/VoiceId.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/VoiceName.cs b/src/Hume/EmpathicVoice/Types/VoiceName.cs index 206a91f..30831a9 100644 --- a/src/Hume/EmpathicVoice/Types/VoiceName.cs +++ b/src/Hume/EmpathicVoice/Types/VoiceName.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/VoiceProvider.cs b/src/Hume/EmpathicVoice/Types/VoiceProvider.cs index 89a59ed..edfbbed 100644 --- a/src/Hume/EmpathicVoice/Types/VoiceProvider.cs +++ b/src/Hume/EmpathicVoice/Types/VoiceProvider.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(VoiceProvider.VoiceProviderSerializer))] [Serializable] public readonly record struct VoiceProvider : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator VoiceProvider(string value) => new(value); + internal class VoiceProviderSerializer : JsonConverter + { + public override VoiceProvider Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new VoiceProvider(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + VoiceProvider value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override VoiceProvider ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new VoiceProvider(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + VoiceProvider value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/WebSocketError.cs b/src/Hume/EmpathicVoice/Types/WebSocketError.cs index b8cf8c1..8b5fbe0 100644 --- a/src/Hume/EmpathicVoice/Types/WebSocketError.cs +++ b/src/Hume/EmpathicVoice/Types/WebSocketError.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -52,12 +52,15 @@ public record WebSocketError : IJsonOnDeserialized /// /// This message indicates a disruption in the WebSocket connection, such as an unexpected disconnection, protocol error, or data transmission issue. /// + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "error"; - set => value.Assert(value == "error", string.Format("'Type' must be {0}", "error")); - } + public WebSocketError.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -70,4 +73,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "error"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/EmpathicVoice/Types/WebhookEventBase.cs b/src/Hume/EmpathicVoice/Types/WebhookEventBase.cs index 9cca5e1..855dc42 100644 --- a/src/Hume/EmpathicVoice/Types/WebhookEventBase.cs +++ b/src/Hume/EmpathicVoice/Types/WebhookEventBase.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/WebhookEventChatEnded.cs b/src/Hume/EmpathicVoice/Types/WebhookEventChatEnded.cs index 12a5df8..74027ed 100644 --- a/src/Hume/EmpathicVoice/Types/WebhookEventChatEnded.cs +++ b/src/Hume/EmpathicVoice/Types/WebhookEventChatEnded.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/WebhookEventChatStartType.cs b/src/Hume/EmpathicVoice/Types/WebhookEventChatStartType.cs index 525e1e1..3a9bd8d 100644 --- a/src/Hume/EmpathicVoice/Types/WebhookEventChatStartType.cs +++ b/src/Hume/EmpathicVoice/Types/WebhookEventChatStartType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WebhookEventChatStartType.WebhookEventChatStartTypeSerializer))] [Serializable] public readonly record struct WebhookEventChatStartType : IStringEnum { @@ -54,6 +55,55 @@ public override string ToString() public static explicit operator WebhookEventChatStartType(string value) => new(value); + internal class WebhookEventChatStartTypeSerializer : JsonConverter + { + public override WebhookEventChatStartType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WebhookEventChatStartType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WebhookEventChatStartType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override WebhookEventChatStartType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new WebhookEventChatStartType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + WebhookEventChatStartType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/WebhookEventChatStarted.cs b/src/Hume/EmpathicVoice/Types/WebhookEventChatStarted.cs index b37e4b4..8347508 100644 --- a/src/Hume/EmpathicVoice/Types/WebhookEventChatStarted.cs +++ b/src/Hume/EmpathicVoice/Types/WebhookEventChatStarted.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/EmpathicVoice/Types/WebhookEventChatStatus.cs b/src/Hume/EmpathicVoice/Types/WebhookEventChatStatus.cs index 59cb6c2..bd25a6c 100644 --- a/src/Hume/EmpathicVoice/Types/WebhookEventChatStatus.cs +++ b/src/Hume/EmpathicVoice/Types/WebhookEventChatStatus.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.EmpathicVoice; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(WebhookEventChatStatus.WebhookEventChatStatusSerializer))] [Serializable] public readonly record struct WebhookEventChatStatus : IStringEnum { @@ -64,6 +65,55 @@ public override string ToString() public static explicit operator WebhookEventChatStatus(string value) => new(value); + internal class WebhookEventChatStatusSerializer : JsonConverter + { + public override WebhookEventChatStatus Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new WebhookEventChatStatus(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + WebhookEventChatStatus value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override WebhookEventChatStatus ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new WebhookEventChatStatus(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + WebhookEventChatStatus value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/EmpathicVoice/Types/WebhookEventToolCall.cs b/src/Hume/EmpathicVoice/Types/WebhookEventToolCall.cs index 2cb7550..de46212 100644 --- a/src/Hume/EmpathicVoice/Types/WebhookEventToolCall.cs +++ b/src/Hume/EmpathicVoice/Types/WebhookEventToolCall.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/BatchClient.cs b/src/Hume/ExpressionMeasurement/Batch/BatchClient.cs index 514d463..9ad7dbf 100644 --- a/src/Hume/ExpressionMeasurement/Batch/BatchClient.cs +++ b/src/Hume/ExpressionMeasurement/Batch/BatchClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; @@ -6,14 +6,14 @@ namespace Hume.ExpressionMeasurement.Batch; public partial class BatchClient : IBatchClient { - private RawClient _client; + private readonly RawClient _client; internal BatchClient(RawClient client) { _client = client; } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse> > ListJobsAsyncCore( BatchListJobsRequest request, @@ -52,7 +52,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize>(responseBody)!; @@ -78,7 +80,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); throw new HumeClientApiException( $"Error with status code {response.StatusCode}", response.StatusCode, @@ -87,7 +91,9 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task> StartInferenceJobAsyncCore( + private async global::System.Threading.Tasks.Task< + WithRawResponse + > StartInferenceJobAsyncCore( InferenceBaseRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -116,7 +122,9 @@ private async System.Threading.Tasks.Task> StartInference .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -142,7 +150,9 @@ private async System.Threading.Tasks.Task> StartInference } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); throw new HumeClientApiException( $"Error with status code {response.StatusCode}", response.StatusCode, @@ -151,7 +161,9 @@ private async System.Threading.Tasks.Task> StartInference } } - private async System.Threading.Tasks.Task> GetJobDetailsAsyncCore( + private async global::System.Threading.Tasks.Task< + WithRawResponse + > GetJobDetailsAsyncCore( string id, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -181,7 +193,9 @@ private async System.Threading.Tasks.Task> GetJobD .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -207,7 +221,9 @@ private async System.Threading.Tasks.Task> GetJobD } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); throw new HumeClientApiException( $"Error with status code {response.StatusCode}", response.StatusCode, @@ -216,7 +232,7 @@ private async System.Threading.Tasks.Task> GetJobD } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse> > GetJobPredictionsAsyncCore( string id, @@ -248,7 +264,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize>( @@ -276,7 +294,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); throw new HumeClientApiException( $"Error with status code {response.StatusCode}", response.StatusCode, @@ -285,8 +305,8 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< - WithRawResponse + private async global::System.Threading.Tasks.Task< + WithRawResponse > GetJobArtifactsAsyncCore( string id, RequestOptions? options = null, @@ -318,7 +338,7 @@ private async System.Threading.Tasks.Task< if (response.StatusCode is >= 200 and < 400) { var stream = await response.Raw.Content.ReadAsStreamAsync(); - return new WithRawResponse() + return new WithRawResponse() { Data = stream, RawResponse = new RawResponse() @@ -330,7 +350,9 @@ private async System.Threading.Tasks.Task< }; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); throw new HumeClientApiException( $"Error with status code {response.StatusCode}", response.StatusCode, @@ -339,7 +361,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > StartInferenceJobFromLocalFileAsyncCore( BatchStartInferenceJobFromLocalFileRequest request, @@ -362,13 +384,15 @@ private async System.Threading.Tasks.Task< Options = options, }; multipartFormRequest_.AddJsonPart("json", request.Json); - multipartFormRequest_.AddFileParameterPart("file", request.File); + multipartFormRequest_.AddFileParameterParts("file", request.File); var response = await _client .SendRequestAsync(multipartFormRequest_, cancellationToken) .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -394,7 +418,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); throw new HumeClientApiException( $"Error with status code {response.StatusCode}", response.StatusCode, @@ -480,13 +506,13 @@ public WithRawResponseTask> GetJobPred /// /// Get the artifacts ZIP of a completed inference job. /// - public WithRawResponseTask GetJobArtifactsAsync( + public WithRawResponseTask GetJobArtifactsAsync( string id, RequestOptions? options = null, CancellationToken cancellationToken = default ) { - return new WithRawResponseTask( + return new WithRawResponseTask( GetJobArtifactsAsyncCore(id, options, cancellationToken) ); } diff --git a/src/Hume/ExpressionMeasurement/Batch/IBatchClient.cs b/src/Hume/ExpressionMeasurement/Batch/IBatchClient.cs index 2f07d74..8b2403d 100644 --- a/src/Hume/ExpressionMeasurement/Batch/IBatchClient.cs +++ b/src/Hume/ExpressionMeasurement/Batch/IBatchClient.cs @@ -43,7 +43,7 @@ WithRawResponseTask> GetJobPredictions /// /// Get the artifacts ZIP of a completed inference job. /// - WithRawResponseTask GetJobArtifactsAsync( + WithRawResponseTask GetJobArtifactsAsync( string id, RequestOptions? options = null, CancellationToken cancellationToken = default diff --git a/src/Hume/ExpressionMeasurement/Batch/Requests/BatchListJobsRequest.cs b/src/Hume/ExpressionMeasurement/Batch/Requests/BatchListJobsRequest.cs index 43ea9f5..921a1e0 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Requests/BatchListJobsRequest.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Requests/BatchListJobsRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; diff --git a/src/Hume/ExpressionMeasurement/Batch/Requests/BatchStartInferenceJobFromLocalFileRequest.cs b/src/Hume/ExpressionMeasurement/Batch/Requests/BatchStartInferenceJobFromLocalFileRequest.cs index 11b9679..ec76c87 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Requests/BatchStartInferenceJobFromLocalFileRequest.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Requests/BatchStartInferenceJobFromLocalFileRequest.cs @@ -11,7 +11,7 @@ public record BatchStartInferenceJobFromLocalFileRequest /// public InferenceBaseRequest? Json { get; set; } - public required FileParameter File { get; set; } + public IEnumerable File { get; set; } = new List(); /// public override string ToString() diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Alternative.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Alternative.cs new file mode 100644 index 0000000..b7c0479 --- /dev/null +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Alternative.cs @@ -0,0 +1,77 @@ +using global::System.Text.Json; +using global::System.Text.Json.Serialization; + +namespace Hume.ExpressionMeasurement.Batch; + +[JsonConverter(typeof(AlternativeConverter))] +public readonly struct Alternative +{ + public const string Value = "language_only"; + + public static implicit operator string(Alternative _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is Alternative; + + public static bool operator ==(Alternative _, Alternative __) => true; + + public static bool operator !=(Alternative _, Alternative __) => false; + + internal sealed class AlternativeConverter : JsonConverter + { + public override Alternative Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != Alternative.Value) + { + throw new JsonException( + "Expected \"" + + Alternative.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new Alternative(); + } + + public override void Write( + Utf8JsonWriter writer, + Alternative value, + JsonSerializerOptions options + ) => writer.WriteStringValue(Alternative.Value); + + public override Alternative ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != Alternative.Value) + { + throw new JsonException( + "Expected \"" + + Alternative.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new Alternative(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Alternative value, + JsonSerializerOptions options + ) => writer.WritePropertyName(Alternative.Value); + } +} diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Bcp47Tag.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Bcp47Tag.cs index 9890642..765cd36 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Bcp47Tag.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Bcp47Tag.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Bcp47Tag.Bcp47TagSerializer))] [Serializable] public readonly record struct Bcp47Tag : IStringEnum { @@ -104,6 +105,55 @@ public override string ToString() public static explicit operator Bcp47Tag(string value) => new(value); + internal class Bcp47TagSerializer : JsonConverter + { + public override Bcp47Tag Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Bcp47Tag(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Bcp47Tag value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override Bcp47Tag ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new Bcp47Tag(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Bcp47Tag value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/BoundingBox.cs b/src/Hume/ExpressionMeasurement/Batch/Types/BoundingBox.cs index e2b5bd5..8efc430 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/BoundingBox.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/BoundingBox.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/BurstPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/BurstPrediction.cs index 7374951..420cf2c 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/BurstPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/BurstPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedEmbeddingGeneration.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedEmbeddingGeneration.cs index 7abc514..ced418a 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedEmbeddingGeneration.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedEmbeddingGeneration.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedInference.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedInference.cs index 83f249d..231f93e 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedInference.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedInference.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedState.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedState.cs index adbea9e..d812bad 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedState.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedState.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedTlInference.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedTlInference.cs index 4074945..9757ad6 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedTlInference.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedTlInference.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedTraining.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedTraining.cs index b505fcb..90d2eff 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CompletedTraining.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CompletedTraining.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelId.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelId.cs index cbe27fd..949bca2 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelId.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelId.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelPrediction.cs index 2fa2306..306dcc2 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelRequest.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelRequest.cs index ca68edf..2060de3 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelRequest.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelRequest.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelVersionId.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelVersionId.cs index f9184f3..67a4849 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelVersionId.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelVersionId.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelsInferenceJob.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelsInferenceJob.cs index cdfba37..6afb3df 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelsInferenceJob.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelsInferenceJob.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelsTrainingJob.cs b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelsTrainingJob.cs index 1b3a192..945045f 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelsTrainingJob.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/CustomModelsTrainingJob.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/DatasetId.cs b/src/Hume/ExpressionMeasurement/Batch/Types/DatasetId.cs index 092370b..0489461 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/DatasetId.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/DatasetId.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/DatasetVersionId.cs b/src/Hume/ExpressionMeasurement/Batch/Types/DatasetVersionId.cs index cc8609a..117fd00 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/DatasetVersionId.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/DatasetVersionId.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/DescriptionsScore.cs b/src/Hume/ExpressionMeasurement/Batch/Types/DescriptionsScore.cs index cbe553f..16c8e99 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/DescriptionsScore.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/DescriptionsScore.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Direction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Direction.cs index 1318210..cd8d741 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Direction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Direction.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Direction.DirectionSerializer))] [Serializable] public readonly record struct Direction : IStringEnum { @@ -50,6 +51,55 @@ public override string ToString() public static explicit operator Direction(string value) => new(value); + internal class DirectionSerializer : JsonConverter + { + public override Direction Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Direction(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Direction value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override Direction ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new Direction(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Direction value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/EmbeddingGenerationBaseRequest.cs b/src/Hume/ExpressionMeasurement/Batch/Types/EmbeddingGenerationBaseRequest.cs index 92dc55f..95c3c35 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/EmbeddingGenerationBaseRequest.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/EmbeddingGenerationBaseRequest.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/EmbeddingGenerationJob.cs b/src/Hume/ExpressionMeasurement/Batch/Types/EmbeddingGenerationJob.cs index a9db384..7cff9ef 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/EmbeddingGenerationJob.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/EmbeddingGenerationJob.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/EmotionScore.cs b/src/Hume/ExpressionMeasurement/Batch/Types/EmotionScore.cs index d367974..d29dc63 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/EmotionScore.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/EmotionScore.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Error.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Error.cs index eb8abcc..06d736c 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Error.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Error.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/EvaluationArgs.cs b/src/Hume/ExpressionMeasurement/Batch/Types/EvaluationArgs.cs index d6c0a8a..7df941d 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/EvaluationArgs.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/EvaluationArgs.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Face.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Face.cs index 91d4850..ba29bf8 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Face.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Face.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/FacePrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/FacePrediction.cs index 349f574..ded76a6 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/FacePrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/FacePrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/FacemeshPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/FacemeshPrediction.cs index 973e375..a0df956 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/FacemeshPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/FacemeshPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/FacsScore.cs b/src/Hume/ExpressionMeasurement/Batch/Types/FacsScore.cs index 7490190..1ce04d3 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/FacsScore.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/FacsScore.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Failed.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Failed.cs index 4036fca..0792ff0 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Failed.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Failed.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/FailedState.cs b/src/Hume/ExpressionMeasurement/Batch/Types/FailedState.cs index ac62e30..7fe6f1e 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/FailedState.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/FailedState.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/File.cs b/src/Hume/ExpressionMeasurement/Batch/Types/File.cs index 6fc83bd..2dd1671 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/File.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/File.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Granularity.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Granularity.cs index f2365f0..4d63898 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Granularity.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Granularity.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Granularity.GranularitySerializer))] [Serializable] public readonly record struct Granularity : IStringEnum { @@ -56,6 +57,55 @@ public override string ToString() public static explicit operator Granularity(string value) => new(value); + internal class GranularitySerializer : JsonConverter + { + public override Granularity Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Granularity(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Granularity value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override Granularity ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new Granularity(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Granularity value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsBurstPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsBurstPrediction.cs index f182d37..1f83d7d 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsBurstPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsBurstPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsFacePrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsFacePrediction.cs index d3110ce..aafad17 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsFacePrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsFacePrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsFacemeshPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsFacemeshPrediction.cs index 4a046d8..0574242 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsFacemeshPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsFacemeshPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsLanguagePrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsLanguagePrediction.cs index df2b957..cd8e266 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsLanguagePrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsLanguagePrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsNerPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsNerPrediction.cs index f77d04b..7cd2885 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsNerPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsNerPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsProsodyPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsProsodyPrediction.cs index 700c051..9b7ed1d 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsProsodyPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/GroupedPredictionsProsodyPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/InProgress.cs b/src/Hume/ExpressionMeasurement/Batch/Types/InProgress.cs index 52afb0b..7fa6dec 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/InProgress.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/InProgress.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/InProgressState.cs b/src/Hume/ExpressionMeasurement/Batch/Types/InProgressState.cs index 0d9f754..f978cf0 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/InProgressState.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/InProgressState.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceBaseRequest.cs b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceBaseRequest.cs index 304701d..eac9c42 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceBaseRequest.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceBaseRequest.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceJob.cs b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceJob.cs index 9420098..02e7c20 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceJob.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceJob.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/InferencePrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/InferencePrediction.cs index b5e00f0..426c05e 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/InferencePrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/InferencePrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceRequest.cs b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceRequest.cs index ea2a1cc..7c8c6b5 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceRequest.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceRequest.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceResults.cs b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceResults.cs index 45c28d2..8cf87c8 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceResults.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceResults.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceSourcePredictResult.cs b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceSourcePredictResult.cs index ffe9a73..e0272ba 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/InferenceSourcePredictResult.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/InferenceSourcePredictResult.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/JobEmbeddingGeneration.cs b/src/Hume/ExpressionMeasurement/Batch/Types/JobEmbeddingGeneration.cs index c1036f4..f02b2a2 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/JobEmbeddingGeneration.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/JobEmbeddingGeneration.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/JobId.cs b/src/Hume/ExpressionMeasurement/Batch/Types/JobId.cs index 1f51d28..d5357e3 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/JobId.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/JobId.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/JobInference.cs b/src/Hume/ExpressionMeasurement/Batch/Types/JobInference.cs index 641b2c0..f955602 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/JobInference.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/JobInference.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/JobTlInference.cs b/src/Hume/ExpressionMeasurement/Batch/Types/JobTlInference.cs index 71bd7b2..5d850bb 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/JobTlInference.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/JobTlInference.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/JobTraining.cs b/src/Hume/ExpressionMeasurement/Batch/Types/JobTraining.cs index ccbf0a7..3677721 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/JobTraining.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/JobTraining.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Language.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Language.cs index 0c2a2d4..05df791 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Language.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Language.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/LanguagePrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/LanguagePrediction.cs index e4008e3..3e13e31 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/LanguagePrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/LanguagePrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Models.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Models.cs index 2af9bed..09d721f 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Models.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Models.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/ModelsPredictions.cs b/src/Hume/ExpressionMeasurement/Batch/Types/ModelsPredictions.cs index 94c1052..5c7d315 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/ModelsPredictions.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/ModelsPredictions.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Ner.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Ner.cs index b2333b4..8277561 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Ner.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Ner.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/NerPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/NerPrediction.cs index 54c01ba..3ceffc5 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/NerPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/NerPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/PositionInterval.cs b/src/Hume/ExpressionMeasurement/Batch/Types/PositionInterval.cs index 70740a1..c5eb0b6 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/PositionInterval.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/PositionInterval.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullBurstPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullBurstPrediction.cs index 2d54966..a4c90b8 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullBurstPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullBurstPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullFacePrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullFacePrediction.cs index f5b88d6..254f3a5 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullFacePrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullFacePrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullFacemeshPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullFacemeshPrediction.cs index c38fbd3..d04a66b 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullFacemeshPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalNullFacemeshPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataLanguagePrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataLanguagePrediction.cs index a6227e9..9d02ea1 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataLanguagePrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataLanguagePrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataNerPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataNerPrediction.cs index 1c0af7b..7d10d87 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataNerPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataNerPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataProsodyPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataProsodyPrediction.cs index 68c702e..507b073 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataProsodyPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/PredictionsOptionalTranscriptionMetadataProsodyPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Prosody.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Prosody.cs index 9e4dd65..39d0757 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Prosody.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Prosody.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/ProsodyPrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/ProsodyPrediction.cs index ccd3275..e2ea50c 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/ProsodyPrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/ProsodyPrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Queued.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Queued.cs index 038b00f..cef7a6e 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Queued.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Queued.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/QueuedState.cs b/src/Hume/ExpressionMeasurement/Batch/Types/QueuedState.cs index 715ab9d..3cb3508 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/QueuedState.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/QueuedState.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/RegistryFileDetail.cs b/src/Hume/ExpressionMeasurement/Batch/Types/RegistryFileDetail.cs index c99ca4b..8173868 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/RegistryFileDetail.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/RegistryFileDetail.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/SentimentScore.cs b/src/Hume/ExpressionMeasurement/Batch/Types/SentimentScore.cs index 0654f92..5fe7825 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/SentimentScore.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/SentimentScore.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/SortBy.cs b/src/Hume/ExpressionMeasurement/Batch/Types/SortBy.cs index 08ce436..33cd2a0 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/SortBy.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/SortBy.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(SortBy.SortBySerializer))] [Serializable] public readonly record struct SortBy : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator SortBy(string value) => new(value); + internal class SortBySerializer : JsonConverter + { + public override SortBy Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new SortBy(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + SortBy value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override SortBy ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new SortBy(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + SortBy value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Source.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Source.cs index 551f8b5..e4907de 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Source.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Source.cs @@ -1,9 +1,9 @@ // ReSharper disable NullableWarningSuppressionIsUsed // ReSharper disable InconsistentNaming -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; @@ -78,7 +78,7 @@ public Source(Source.Text value) public Hume.ExpressionMeasurement.Batch.SourceUrl AsUrl() => IsUrl ? (Hume.ExpressionMeasurement.Batch.SourceUrl)Value! - : throw new System.Exception("Source.Type is not 'url'"); + : throw new global::System.Exception("Source.Type is not 'url'"); /// /// Returns the value as a if is 'file', otherwise throws an exception. @@ -87,7 +87,7 @@ public Hume.ExpressionMeasurement.Batch.SourceUrl AsUrl() => public Hume.ExpressionMeasurement.Batch.SourceFile AsFile() => IsFile ? (Hume.ExpressionMeasurement.Batch.SourceFile)Value! - : throw new System.Exception("Source.Type is not 'file'"); + : throw new global::System.Exception("Source.Type is not 'file'"); /// /// Returns the value as a if is 'text', otherwise throws an exception. @@ -96,7 +96,7 @@ public Hume.ExpressionMeasurement.Batch.SourceFile AsFile() => public Hume.ExpressionMeasurement.Batch.SourceTextSource AsText() => IsText ? (Hume.ExpressionMeasurement.Batch.SourceTextSource)Value! - : throw new System.Exception("Source.Type is not 'text'"); + : throw new global::System.Exception("Source.Type is not 'text'"); public T Match( Func onUrl, @@ -191,12 +191,12 @@ public bool TryAsText(out Hume.ExpressionMeasurement.Batch.SourceTextSource? val [Serializable] internal sealed class JsonConverter : JsonConverter { - public override bool CanConvert(System.Type typeToConvert) => + public override bool CanConvert(global::System.Type typeToConvert) => typeof(Source).IsAssignableFrom(typeToConvert); public override Source Read( ref Utf8JsonReader reader, - System.Type typeToConvert, + global::System.Type typeToConvert, JsonSerializerOptions options ) { @@ -272,6 +272,27 @@ JsonSerializerOptions options json["type"] = value.Type; json.WriteTo(writer, options); } + + public override Source ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new JsonException("The JSON property name could not be read as a string."); + return new Source(stringValue, stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Source value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Type); + } } /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/SourceFile.cs b/src/Hume/ExpressionMeasurement/Batch/Types/SourceFile.cs index 1b0bc8f..d4101a4 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/SourceFile.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/SourceFile.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/SourceTextSource.cs b/src/Hume/ExpressionMeasurement/Batch/Types/SourceTextSource.cs index be97a2f..81d11ae 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/SourceTextSource.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/SourceTextSource.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/SourceUrl.cs b/src/Hume/ExpressionMeasurement/Batch/Types/SourceUrl.cs index 644898c..402a52f 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/SourceUrl.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/SourceUrl.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGeneration.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGeneration.cs index 74dc331..80cda6d 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGeneration.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGeneration.cs @@ -1,9 +1,9 @@ // ReSharper disable NullableWarningSuppressionIsUsed // ReSharper disable InconsistentNaming -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; @@ -92,7 +92,7 @@ public StateEmbeddingGeneration(StateEmbeddingGeneration.Failed value) public Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationQueued AsQueued() => IsQueued ? (Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationQueued)Value! - : throw new System.Exception("StateEmbeddingGeneration.Status is not 'QUEUED'"); + : throw new global::System.Exception("StateEmbeddingGeneration.Status is not 'QUEUED'"); /// /// Returns the value as a if is 'IN_PROGRESS', otherwise throws an exception. @@ -101,7 +101,9 @@ public Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationQueued AsQueued( public Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationInProgress AsInProgress() => IsInProgress ? (Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationInProgress)Value! - : throw new System.Exception("StateEmbeddingGeneration.Status is not 'IN_PROGRESS'"); + : throw new global::System.Exception( + "StateEmbeddingGeneration.Status is not 'IN_PROGRESS'" + ); /// /// Returns the value as a if is 'COMPLETED', otherwise throws an exception. @@ -111,7 +113,9 @@ public Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationCompletedEmbeddi IsCompleted ? (Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationCompletedEmbeddingGeneration) Value! - : throw new System.Exception("StateEmbeddingGeneration.Status is not 'COMPLETED'"); + : throw new global::System.Exception( + "StateEmbeddingGeneration.Status is not 'COMPLETED'" + ); /// /// Returns the value as a if is 'FAILED', otherwise throws an exception. @@ -120,7 +124,7 @@ public Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationCompletedEmbeddi public Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationFailed AsFailed() => IsFailed ? (Hume.ExpressionMeasurement.Batch.StateEmbeddingGenerationFailed)Value! - : throw new System.Exception("StateEmbeddingGeneration.Status is not 'FAILED'"); + : throw new global::System.Exception("StateEmbeddingGeneration.Status is not 'FAILED'"); public T Match( Func onQueued, @@ -258,12 +262,12 @@ StateEmbeddingGeneration.Failed value [Serializable] internal sealed class JsonConverter : JsonConverter { - public override bool CanConvert(System.Type typeToConvert) => + public override bool CanConvert(global::System.Type typeToConvert) => typeof(StateEmbeddingGeneration).IsAssignableFrom(typeToConvert); public override StateEmbeddingGeneration Read( ref Utf8JsonReader reader, - System.Type typeToConvert, + global::System.Type typeToConvert, JsonSerializerOptions options ) { @@ -347,6 +351,27 @@ JsonSerializerOptions options json["status"] = value.Status; json.WriteTo(writer, options); } + + public override StateEmbeddingGeneration ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new JsonException("The JSON property name could not be read as a string."); + return new StateEmbeddingGeneration(stringValue, stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + StateEmbeddingGeneration value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Status); + } } /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationCompletedEmbeddingGeneration.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationCompletedEmbeddingGeneration.cs index ac6b5da..b3b4cc4 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationCompletedEmbeddingGeneration.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationCompletedEmbeddingGeneration.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationFailed.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationFailed.cs index 08dd17d..ecc0658 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationFailed.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationFailed.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationInProgress.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationInProgress.cs index 16fa293..90e9d5f 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationInProgress.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationInProgress.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationQueued.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationQueued.cs index 20ab240..bcd2cb9 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationQueued.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateEmbeddingGenerationQueued.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateInference.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateInference.cs index 0b86c53..bc84ce3 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateInference.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateInference.cs @@ -1,9 +1,9 @@ // ReSharper disable NullableWarningSuppressionIsUsed // ReSharper disable InconsistentNaming -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; @@ -92,7 +92,7 @@ public StateInference(StateInference.Failed value) public Hume.ExpressionMeasurement.Batch.QueuedState AsQueued() => IsQueued ? (Hume.ExpressionMeasurement.Batch.QueuedState)Value! - : throw new System.Exception("StateInference.Status is not 'QUEUED'"); + : throw new global::System.Exception("StateInference.Status is not 'QUEUED'"); /// /// Returns the value as a if is 'IN_PROGRESS', otherwise throws an exception. @@ -101,7 +101,7 @@ public Hume.ExpressionMeasurement.Batch.QueuedState AsQueued() => public Hume.ExpressionMeasurement.Batch.InProgressState AsInProgress() => IsInProgress ? (Hume.ExpressionMeasurement.Batch.InProgressState)Value! - : throw new System.Exception("StateInference.Status is not 'IN_PROGRESS'"); + : throw new global::System.Exception("StateInference.Status is not 'IN_PROGRESS'"); /// /// Returns the value as a if is 'COMPLETED', otherwise throws an exception. @@ -110,7 +110,7 @@ public Hume.ExpressionMeasurement.Batch.InProgressState AsInProgress() => public Hume.ExpressionMeasurement.Batch.CompletedState AsCompleted() => IsCompleted ? (Hume.ExpressionMeasurement.Batch.CompletedState)Value! - : throw new System.Exception("StateInference.Status is not 'COMPLETED'"); + : throw new global::System.Exception("StateInference.Status is not 'COMPLETED'"); /// /// Returns the value as a if is 'FAILED', otherwise throws an exception. @@ -119,7 +119,7 @@ public Hume.ExpressionMeasurement.Batch.CompletedState AsCompleted() => public Hume.ExpressionMeasurement.Batch.FailedState AsFailed() => IsFailed ? (Hume.ExpressionMeasurement.Batch.FailedState)Value! - : throw new System.Exception("StateInference.Status is not 'FAILED'"); + : throw new global::System.Exception("StateInference.Status is not 'FAILED'"); public T Match( Func onQueued, @@ -236,12 +236,12 @@ public bool TryAsFailed(out Hume.ExpressionMeasurement.Batch.FailedState? value) [Serializable] internal sealed class JsonConverter : JsonConverter { - public override bool CanConvert(System.Type typeToConvert) => + public override bool CanConvert(global::System.Type typeToConvert) => typeof(StateInference).IsAssignableFrom(typeToConvert); public override StateInference Read( ref Utf8JsonReader reader, - System.Type typeToConvert, + global::System.Type typeToConvert, JsonSerializerOptions options ) { @@ -325,6 +325,27 @@ JsonSerializerOptions options json["status"] = value.Status; json.WriteTo(writer, options); } + + public override StateInference ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new JsonException("The JSON property name could not be read as a string."); + return new StateInference(stringValue, stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + StateInference value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Status); + } } /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInference.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInference.cs index 12859b0..0015bca 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInference.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInference.cs @@ -1,9 +1,9 @@ // ReSharper disable NullableWarningSuppressionIsUsed // ReSharper disable InconsistentNaming -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; @@ -92,7 +92,7 @@ public StateTlInference(StateTlInference.Failed value) public Hume.ExpressionMeasurement.Batch.StateTlInferenceQueued AsQueued() => IsQueued ? (Hume.ExpressionMeasurement.Batch.StateTlInferenceQueued)Value! - : throw new System.Exception("StateTlInference.Status is not 'QUEUED'"); + : throw new global::System.Exception("StateTlInference.Status is not 'QUEUED'"); /// /// Returns the value as a if is 'IN_PROGRESS', otherwise throws an exception. @@ -101,7 +101,7 @@ public Hume.ExpressionMeasurement.Batch.StateTlInferenceQueued AsQueued() => public Hume.ExpressionMeasurement.Batch.StateTlInferenceInProgress AsInProgress() => IsInProgress ? (Hume.ExpressionMeasurement.Batch.StateTlInferenceInProgress)Value! - : throw new System.Exception("StateTlInference.Status is not 'IN_PROGRESS'"); + : throw new global::System.Exception("StateTlInference.Status is not 'IN_PROGRESS'"); /// /// Returns the value as a if is 'COMPLETED', otherwise throws an exception. @@ -110,7 +110,7 @@ public Hume.ExpressionMeasurement.Batch.StateTlInferenceInProgress AsInProgress( public Hume.ExpressionMeasurement.Batch.StateTlInferenceCompletedTlInference AsCompleted() => IsCompleted ? (Hume.ExpressionMeasurement.Batch.StateTlInferenceCompletedTlInference)Value! - : throw new System.Exception("StateTlInference.Status is not 'COMPLETED'"); + : throw new global::System.Exception("StateTlInference.Status is not 'COMPLETED'"); /// /// Returns the value as a if is 'FAILED', otherwise throws an exception. @@ -119,7 +119,7 @@ public Hume.ExpressionMeasurement.Batch.StateTlInferenceCompletedTlInference AsC public Hume.ExpressionMeasurement.Batch.StateTlInferenceFailed AsFailed() => IsFailed ? (Hume.ExpressionMeasurement.Batch.StateTlInferenceFailed)Value! - : throw new System.Exception("StateTlInference.Status is not 'FAILED'"); + : throw new global::System.Exception("StateTlInference.Status is not 'FAILED'"); public T Match( Func onQueued, @@ -242,12 +242,12 @@ public static implicit operator StateTlInference(StateTlInference.Completed valu [Serializable] internal sealed class JsonConverter : JsonConverter { - public override bool CanConvert(System.Type typeToConvert) => + public override bool CanConvert(global::System.Type typeToConvert) => typeof(StateTlInference).IsAssignableFrom(typeToConvert); public override StateTlInference Read( ref Utf8JsonReader reader, - System.Type typeToConvert, + global::System.Type typeToConvert, JsonSerializerOptions options ) { @@ -331,6 +331,27 @@ JsonSerializerOptions options json["status"] = value.Status; json.WriteTo(writer, options); } + + public override StateTlInference ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new JsonException("The JSON property name could not be read as a string."); + return new StateTlInference(stringValue, stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + StateTlInference value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Status); + } } /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceCompletedTlInference.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceCompletedTlInference.cs index b8732db..74ecb10 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceCompletedTlInference.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceCompletedTlInference.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceFailed.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceFailed.cs index 86b47e7..689a2fd 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceFailed.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceFailed.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceInProgress.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceInProgress.cs index be69778..67a0e0c 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceInProgress.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceInProgress.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceQueued.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceQueued.cs index 841af45..66975ee 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceQueued.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTlInferenceQueued.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTraining.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTraining.cs index 4355543..6590792 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTraining.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTraining.cs @@ -1,9 +1,9 @@ // ReSharper disable NullableWarningSuppressionIsUsed // ReSharper disable InconsistentNaming -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; @@ -92,7 +92,7 @@ public StateTraining(StateTraining.Failed value) public Hume.ExpressionMeasurement.Batch.StateTrainingQueued AsQueued() => IsQueued ? (Hume.ExpressionMeasurement.Batch.StateTrainingQueued)Value! - : throw new System.Exception("StateTraining.Status is not 'QUEUED'"); + : throw new global::System.Exception("StateTraining.Status is not 'QUEUED'"); /// /// Returns the value as a if is 'IN_PROGRESS', otherwise throws an exception. @@ -101,7 +101,7 @@ public Hume.ExpressionMeasurement.Batch.StateTrainingQueued AsQueued() => public Hume.ExpressionMeasurement.Batch.StateTrainingInProgress AsInProgress() => IsInProgress ? (Hume.ExpressionMeasurement.Batch.StateTrainingInProgress)Value! - : throw new System.Exception("StateTraining.Status is not 'IN_PROGRESS'"); + : throw new global::System.Exception("StateTraining.Status is not 'IN_PROGRESS'"); /// /// Returns the value as a if is 'COMPLETED', otherwise throws an exception. @@ -110,7 +110,7 @@ public Hume.ExpressionMeasurement.Batch.StateTrainingInProgress AsInProgress() = public Hume.ExpressionMeasurement.Batch.StateTrainingCompletedTraining AsCompleted() => IsCompleted ? (Hume.ExpressionMeasurement.Batch.StateTrainingCompletedTraining)Value! - : throw new System.Exception("StateTraining.Status is not 'COMPLETED'"); + : throw new global::System.Exception("StateTraining.Status is not 'COMPLETED'"); /// /// Returns the value as a if is 'FAILED', otherwise throws an exception. @@ -119,7 +119,7 @@ public Hume.ExpressionMeasurement.Batch.StateTrainingCompletedTraining AsComplet public Hume.ExpressionMeasurement.Batch.StateTrainingFailed AsFailed() => IsFailed ? (Hume.ExpressionMeasurement.Batch.StateTrainingFailed)Value! - : throw new System.Exception("StateTraining.Status is not 'FAILED'"); + : throw new global::System.Exception("StateTraining.Status is not 'FAILED'"); public T Match( Func onQueued, @@ -238,12 +238,12 @@ public bool TryAsFailed(out Hume.ExpressionMeasurement.Batch.StateTrainingFailed [Serializable] internal sealed class JsonConverter : JsonConverter { - public override bool CanConvert(System.Type typeToConvert) => + public override bool CanConvert(global::System.Type typeToConvert) => typeof(StateTraining).IsAssignableFrom(typeToConvert); public override StateTraining Read( ref Utf8JsonReader reader, - System.Type typeToConvert, + global::System.Type typeToConvert, JsonSerializerOptions options ) { @@ -327,6 +327,27 @@ JsonSerializerOptions options json["status"] = value.Status; json.WriteTo(writer, options); } + + public override StateTraining ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new JsonException("The JSON property name could not be read as a string."); + return new StateTraining(stringValue, stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + StateTraining value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Status); + } } /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingCompletedTraining.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingCompletedTraining.cs index 89cb646..cbd9ca9 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingCompletedTraining.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingCompletedTraining.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingFailed.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingFailed.cs index 6500104..7d96430 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingFailed.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingFailed.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingInProgress.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingInProgress.cs index d763b74..3735946 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingInProgress.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingInProgress.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingQueued.cs b/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingQueued.cs index a6bdd7f..1d97d6e 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingQueued.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/StateTrainingQueued.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Status.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Status.cs index fcda448..3cf24ca 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Status.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Status.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Status.StatusSerializer))] [Serializable] public readonly record struct Status : IStringEnum { @@ -54,6 +55,55 @@ public override string ToString() public static explicit operator Status(string value) => new(value); + internal class StatusSerializer : JsonConverter + { + public override Status Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Status(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + Status value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override Status ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new Status(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Status value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Tag.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Tag.cs index 4f2760e..3d8d9d1 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Tag.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Tag.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Task.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Task.cs index 6f5deb3..4d6434f 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Task.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Task.cs @@ -1,9 +1,9 @@ // ReSharper disable NullableWarningSuppressionIsUsed // ReSharper disable InconsistentNaming -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Nodes; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; @@ -64,7 +64,7 @@ public Task(Task.Regression value) public Hume.ExpressionMeasurement.Batch.TaskClassification AsClassification() => IsClassification ? (Hume.ExpressionMeasurement.Batch.TaskClassification)Value! - : throw new System.Exception("Task.Type is not 'classification'"); + : throw new global::System.Exception("Task.Type is not 'classification'"); /// /// Returns the value as a if is 'regression', otherwise throws an exception. @@ -73,7 +73,7 @@ public Hume.ExpressionMeasurement.Batch.TaskClassification AsClassification() => public Hume.ExpressionMeasurement.Batch.TaskRegression AsRegression() => IsRegression ? (Hume.ExpressionMeasurement.Batch.TaskRegression)Value! - : throw new System.Exception("Task.Type is not 'regression'"); + : throw new global::System.Exception("Task.Type is not 'regression'"); public T Match( Func onClassification, @@ -146,12 +146,12 @@ public bool TryAsRegression(out Hume.ExpressionMeasurement.Batch.TaskRegression? [Serializable] internal sealed class JsonConverter : JsonConverter { - public override bool CanConvert(System.Type typeToConvert) => + public override bool CanConvert(global::System.Type typeToConvert) => typeof(Task).IsAssignableFrom(typeToConvert); public override Task Read( ref Utf8JsonReader reader, - System.Type typeToConvert, + global::System.Type typeToConvert, JsonSerializerOptions options ) { @@ -215,6 +215,27 @@ public override void Write(Utf8JsonWriter writer, Task value, JsonSerializerOpti json["type"] = value.Type; json.WriteTo(writer, options); } + + public override Task ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new JsonException("The JSON property name could not be read as a string."); + return new Task(stringValue, stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Task value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Type); + } } /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TaskClassification.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TaskClassification.cs index 627b79f..2171466 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TaskClassification.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TaskClassification.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TaskRegression.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TaskRegression.cs index 20bcc82..ba28356 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TaskRegression.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TaskRegression.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TimeInterval.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TimeInterval.cs index f31120b..9a36df9 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TimeInterval.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TimeInterval.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceBaseRequest.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceBaseRequest.cs index bdedf04..a036e4d 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceBaseRequest.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceBaseRequest.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TlInferencePrediction.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TlInferencePrediction.cs index ec30fc0..2faa9eb 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TlInferencePrediction.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TlInferencePrediction.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceResults.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceResults.cs index 0b06a9f..10b2248 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceResults.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceResults.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceSourcePredictResult.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceSourcePredictResult.cs index c63fc5b..6ad270f 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceSourcePredictResult.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TlInferenceSourcePredictResult.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/ToxicityScore.cs b/src/Hume/ExpressionMeasurement/Batch/Types/ToxicityScore.cs index bbea53c..88a5400 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/ToxicityScore.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/ToxicityScore.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TrainingBaseRequest.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TrainingBaseRequest.cs index e37c000..481f903 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TrainingBaseRequest.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TrainingBaseRequest.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TrainingCustomModel.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TrainingCustomModel.cs index 480496b..1de7aad 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TrainingCustomModel.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TrainingCustomModel.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Transcription.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Transcription.cs index 3f9673f..0f3b613 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Transcription.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Transcription.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/TranscriptionMetadata.cs b/src/Hume/ExpressionMeasurement/Batch/Types/TranscriptionMetadata.cs index 6a35fe4..dc8a436 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/TranscriptionMetadata.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/TranscriptionMetadata.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Type.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Type.cs index 31b92dd..01afded 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Type.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Type.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(Type.TypeSerializer))] [Serializable] public readonly record struct Type : IStringEnum { @@ -54,6 +55,51 @@ public override string ToString() public static explicit operator Type(string value) => new(value); + internal class TypeSerializer : JsonConverter + { + public override Type Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new Type(stringValue); + } + + public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + + public override Type ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new Type(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + Type value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Url.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Url.cs index 680e27f..ffe4afe 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Url.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Url.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/ValidationArgs.cs b/src/Hume/ExpressionMeasurement/Batch/Types/ValidationArgs.cs index 1c5bca8..de67754 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/ValidationArgs.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/ValidationArgs.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/When.cs b/src/Hume/ExpressionMeasurement/Batch/Types/When.cs index 6f766b6..84226b3 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/When.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/When.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.ExpressionMeasurement.Batch; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(When.WhenSerializer))] [Serializable] public readonly record struct When : IStringEnum { @@ -50,6 +51,51 @@ public override string ToString() public static explicit operator When(string value) => new(value); + internal class WhenSerializer : JsonConverter + { + public override When Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new When(stringValue); + } + + public override void Write(Utf8JsonWriter writer, When value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value); + } + + public override When ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new When(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + When value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/ExpressionMeasurement/Batch/Types/Window.cs b/src/Hume/ExpressionMeasurement/Batch/Types/Window.cs index 76c6933..065a167 100644 --- a/src/Hume/ExpressionMeasurement/Batch/Types/Window.cs +++ b/src/Hume/ExpressionMeasurement/Batch/Types/Window.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/ExpressionMeasurementClient.cs b/src/Hume/ExpressionMeasurement/ExpressionMeasurementClient.cs index bdf985f..333af40 100644 --- a/src/Hume/ExpressionMeasurement/ExpressionMeasurementClient.cs +++ b/src/Hume/ExpressionMeasurement/ExpressionMeasurementClient.cs @@ -6,7 +6,7 @@ namespace Hume.ExpressionMeasurement; public partial class ExpressionMeasurementClient : IExpressionMeasurementClient { - private RawClient _client; + private readonly RawClient _client; internal ExpressionMeasurementClient(RawClient client) { diff --git a/src/Hume/ExpressionMeasurement/Stream/IStreamClient.cs b/src/Hume/ExpressionMeasurement/Stream/IStreamClient.cs index 82b0be5..33b36c9 100644 --- a/src/Hume/ExpressionMeasurement/Stream/IStreamClient.cs +++ b/src/Hume/ExpressionMeasurement/Stream/IStreamClient.cs @@ -1,3 +1,8 @@ namespace Hume.ExpressionMeasurement.Stream; -public partial interface IStreamClient { } +public partial interface IStreamClient +{ + IStreamApi CreateStreamApi(); + + IStreamApi CreateStreamApi(StreamApi.Options options); +} diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/IStreamApi.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/IStreamApi.cs new file mode 100644 index 0000000..1658240 --- /dev/null +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/IStreamApi.cs @@ -0,0 +1,20 @@ +using Hume.Core.WebSockets; + +namespace Hume.ExpressionMeasurement.Stream; + +public partial interface IStreamApi : IAsyncDisposable, IDisposable +{ + public Event Connected { get; } + public Event Closed { get; } + public Event ExceptionOccurred { get; } + public Event Reconnecting { get; } + public ConnectionStatus Status { get; } + global::System.Threading.Tasks.Task ConnectAsync(CancellationToken cancellationToken = default); + + global::System.Threading.Tasks.Task Send( + StreamModelsEndpointPayload message, + CancellationToken cancellationToken = default + ); + + global::System.Threading.Tasks.Task CloseAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/StreamApi.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/StreamApi.cs index 7f02ab7..fb19865 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/StreamApi.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/StreamApi.cs @@ -1,11 +1,11 @@ -using System.ComponentModel; -using System.Text.Json; +using global::System.ComponentModel; +using global::System.Text.Json; using Hume.Core; using Hume.Core.WebSockets; namespace Hume.ExpressionMeasurement.Stream; -public partial class StreamApi : IAsyncDisposable, IDisposable, INotifyPropertyChanged +public partial class StreamApi : IStreamApi, IAsyncDisposable, IDisposable, INotifyPropertyChanged { private readonly StreamApi.Options _options; @@ -38,6 +38,12 @@ public event PropertyChangedEventHandler PropertyChanged /// public readonly Event StreamWarningMessage = new(); + /// + /// Event handler for unknown/unrecognized message types. + /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. + /// + public readonly Event UnknownMessage = new(); + /// /// Default constructor /// @@ -52,6 +58,12 @@ public StreamApi(StreamApi.Options options) var uri = new UriBuilder(_options.BaseUrl); uri.Path = $"{uri.Path.TrimEnd('/')}/models"; _client = new WebSocketClient(uri.Uri, OnTextMessage); + _client.HttpInvoker = _options.HttpInvoker; + _client.IsReconnectionEnabled = _options.IsReconnectionEnabled; + _client.ReconnectTimeout = _options.ReconnectTimeout; + _client.ErrorReconnectTimeout = _options.ErrorReconnectTimeout; + _client.LostReconnectTimeout = _options.LostReconnectTimeout; + _client.Backoff = _options.ReconnectBackoff; } /// @@ -74,6 +86,11 @@ public StreamApi(StreamApi.Options options) /// public Event ExceptionOccurred => _client.ExceptionOccurred; + /// + /// Event raised when the WebSocket connection is re-established after a disconnect. + /// + public Event Reconnecting => _client.Reconnecting; + /// /// Disposes of event subscriptions /// @@ -82,14 +99,15 @@ private void DisposeEvents() StreamModelPredictions.Dispose(); StreamErrorMessage.Dispose(); StreamWarningMessage.Dispose(); + UnknownMessage.Dispose(); } /// /// Dispatches incoming WebSocket messages /// - private async System.Threading.Tasks.Task OnTextMessage(System.IO.Stream stream) + private async global::System.Threading.Tasks.Task OnTextMessage(global::System.IO.Stream stream) { - var json = await JsonSerializer.DeserializeAsync(stream); + using var json = await JsonSerializer.DeserializeAsync(stream); if (json == null) { await ExceptionOccurred @@ -123,25 +141,54 @@ await ExceptionOccurred } } - await ExceptionOccurred - .RaiseEvent(new Exception($"Unknown message: {json.ToString()}")) + await UnknownMessage.RaiseEvent(json.RootElement.Clone()).ConfigureAwait(false); + } + + /// + /// Serializes and sends a JSON message to the server + /// + private async global::System.Threading.Tasks.Task SendJsonAsync( + object message, + CancellationToken cancellationToken = default + ) + { + await _client + .SendInstant(JsonUtils.Serialize(message), cancellationToken) .ConfigureAwait(false); } + /// + /// Injects a fake text message for testing. Dispatches through the normal message handling pipeline. + /// + internal async global::System.Threading.Tasks.Task InjectTestMessage(string rawJson) + { + using var stream = new MemoryStream(global::System.Text.Encoding.UTF8.GetBytes(rawJson)); + await OnTextMessage(stream).ConfigureAwait(false); + } + /// /// Asynchronously establishes a WebSocket connection. /// - public async System.Threading.Tasks.Task ConnectAsync() + public async global::System.Threading.Tasks.Task ConnectAsync( + CancellationToken cancellationToken = default + ) { - await _client.ConnectAsync().ConfigureAwait(false); +#if NET6_0_OR_GREATER + _client.DeflateOptions = _options.EnableCompression + ? new System.Net.WebSockets.WebSocketDeflateOptions() + : null; +#endif + await _client.ConnectAsync(cancellationToken).ConfigureAwait(false); } /// /// Asynchronously closes the WebSocket connection. /// - public async System.Threading.Tasks.Task CloseAsync() + public async global::System.Threading.Tasks.Task CloseAsync( + CancellationToken cancellationToken = default + ) { - await _client.CloseAsync().ConfigureAwait(false); + await _client.CloseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -167,9 +214,12 @@ public void Dispose() /// /// Sends a StreamModelsEndpointPayload message to the server /// - public async System.Threading.Tasks.Task Send(StreamModelsEndpointPayload message) + public async global::System.Threading.Tasks.Task Send( + StreamModelsEndpointPayload message, + CancellationToken cancellationToken = default + ) { - await _client.SendInstant(JsonUtils.Serialize(message)).ConfigureAwait(false); + await SendJsonAsync(message, cancellationToken).ConfigureAwait(false); } /// @@ -181,5 +231,41 @@ public class Options /// The Websocket URL for the API connection. /// public string BaseUrl { get; set; } = "wss://api.hume.ai/v0/stream"; + + /// + /// Enable per-message deflate compression (RFC 7692). When true, the client sets ClientWebSocketOptions.DangerousDeflateOptions before connecting. Compression is negotiated during the handshake; if the server does not support it, the connection proceeds uncompressed. Default: false. + /// Security warning: do not enable compression when transmitting data containing secrets — compressed encrypted payloads are vulnerable to CRIME/BREACH side-channel attacks. See ClientWebSocketOptions.DangerousDeflateOptions for details. + /// + public bool EnableCompression { get; set; } = false; + + /// + /// Optional HTTP/2 handler for multiplexed WebSocket connections (.NET 7+). + /// + public System.Net.Http.HttpMessageInvoker? HttpInvoker { get; set; } + + /// + /// Enable or disable automatic reconnection. Default: false. + /// + public bool IsReconnectionEnabled { get; set; } = false; + + /// + /// Time to wait before reconnecting if no message comes from the server. Set null to disable. Default: 1 minute. + /// + public TimeSpan? ReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the last reconnection attempt failed. Set null to disable. Default: 1 minute. + /// + public TimeSpan? ErrorReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the connection is lost with a transient error. Set null to disable (reconnect immediately). Default: null. + /// + public TimeSpan? LostReconnectTimeout { get; set; } + + /// + /// Backoff strategy for reconnection delays. Controls interval growth, jitter, and max attempts. Set to null to use fixed-interval reconnection (legacy behavior). Default: exponential backoff, 1s→60s, unlimited attempts, with jitter. + /// + public ReconnectStrategy? ReconnectBackoff { get; set; } = new ReconnectStrategy(); } } diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/Config.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/Config.cs index e80a506..8b697c0 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/Config.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/Config.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/JobDetails.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/JobDetails.cs index c765ce0..2a0324e 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/JobDetails.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/JobDetails.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamErrorMessage.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamErrorMessage.cs index 8395123..aee4f0a 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamErrorMessage.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamErrorMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamFace.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamFace.cs index b68739a..044099d 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamFace.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamFace.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamLanguage.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamLanguage.cs index 6b192f0..8464bb5 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamLanguage.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamLanguage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictions.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictions.cs index f868420..93eae9a 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictions.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictions.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsBurst.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsBurst.cs index 58993c2..6d56567 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsBurst.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsBurst.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsBurstPredictionsItem.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsBurstPredictionsItem.cs index 86a09a9..587942b 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsBurstPredictionsItem.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsBurstPredictionsItem.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFace.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFace.cs index 767c933..92f184e 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFace.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFace.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacePredictionsItem.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacePredictionsItem.cs index 7ae1e97..3ab0cdd 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacePredictionsItem.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacePredictionsItem.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacemesh.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacemesh.cs index f2c49c8..cd8b023 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacemesh.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacemesh.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacemeshPredictionsItem.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacemeshPredictionsItem.cs index d7cc090..fcbbae2 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacemeshPredictionsItem.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsFacemeshPredictionsItem.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsJobDetails.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsJobDetails.cs index f9d6cd4..c5d1b24 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsJobDetails.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsJobDetails.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsLanguage.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsLanguage.cs index 9b03c10..8a94a72 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsLanguage.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsLanguage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsLanguagePredictionsItem.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsLanguagePredictionsItem.cs index c57092e..d3061f9 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsLanguagePredictionsItem.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsLanguagePredictionsItem.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsProsody.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsProsody.cs index 25f926d..8ea64f3 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsProsody.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsProsody.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsProsodyPredictionsItem.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsProsodyPredictionsItem.cs index 07cd929..84b71ea 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsProsodyPredictionsItem.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelPredictionsProsodyPredictionsItem.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelsEndpointPayload.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelsEndpointPayload.cs index 5318eff..6cb3d32 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelsEndpointPayload.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamModelsEndpointPayload.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamWarningMessage.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamWarningMessage.cs index 3cf8693..1f58864 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamWarningMessage.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamWarningMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamWarningMessageJobDetails.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamWarningMessageJobDetails.cs index b98ce53..b9acd94 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamWarningMessageJobDetails.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/Types/StreamWarningMessageJobDetails.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/StreamClient.cs b/src/Hume/ExpressionMeasurement/Stream/StreamClient.cs index 1359a8b..62e3281 100644 --- a/src/Hume/ExpressionMeasurement/Stream/StreamClient.cs +++ b/src/Hume/ExpressionMeasurement/Stream/StreamClient.cs @@ -4,19 +4,21 @@ namespace Hume.ExpressionMeasurement.Stream; public partial class StreamClient : IStreamClient { - private RawClient _client; + private readonly RawClient _client; internal StreamClient(RawClient client) { _client = client; } - public StreamApi CreateStreamApi() + public IStreamApi CreateStreamApi() { - return new StreamApi(new StreamApi.Options()); + return new StreamApi( + new StreamApi.Options { BaseUrl = _client.Options.Environment.Stream } + ); } - public StreamApi CreateStreamApi(StreamApi.Options options) + public IStreamApi CreateStreamApi(StreamApi.Options options) { return new StreamApi(options); } diff --git a/src/Hume/ExpressionMeasurement/Stream/Types/EmotionEmbeddingItem.cs b/src/Hume/ExpressionMeasurement/Stream/Types/EmotionEmbeddingItem.cs index 88e4270..49f0c7b 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Types/EmotionEmbeddingItem.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Types/EmotionEmbeddingItem.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Types/SentimentItem.cs b/src/Hume/ExpressionMeasurement/Stream/Types/SentimentItem.cs index 26f9d11..fb3fc6a 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Types/SentimentItem.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Types/SentimentItem.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Types/StreamBoundingBox.cs b/src/Hume/ExpressionMeasurement/Stream/Types/StreamBoundingBox.cs index 13de4ae..586b5a3 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Types/StreamBoundingBox.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Types/StreamBoundingBox.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Types/TextPosition.cs b/src/Hume/ExpressionMeasurement/Stream/Types/TextPosition.cs index 204a407..63bc178 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Types/TextPosition.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Types/TextPosition.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Types/TimeRange.cs b/src/Hume/ExpressionMeasurement/Stream/Types/TimeRange.cs index bf87cdb..c7a8d92 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Types/TimeRange.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Types/TimeRange.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/ExpressionMeasurement/Stream/Types/ToxicityItem.cs b/src/Hume/ExpressionMeasurement/Stream/Types/ToxicityItem.cs index f837faf..1a3315e 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Types/ToxicityItem.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Types/ToxicityItem.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Hume.csproj b/src/Hume/Hume.csproj index 93d3b31..58e946e 100644 --- a/src/Hume/Hume.csproj +++ b/src/Hume/Hume.csproj @@ -1,10 +1,10 @@ - net462;net8.0;netstandard2.0 + net462;net8.0;net9.0;netstandard2.0 enable 12 enable - 0.2.6 + 0.2.7 $(Version) $(Version) README.md @@ -27,7 +27,7 @@ - + @@ -39,12 +39,14 @@ - - - + + + + + diff --git a/src/Hume/HumeClient.cs b/src/Hume/HumeClient.cs index 306cb83..6bca1e4 100644 --- a/src/Hume/HumeClient.cs +++ b/src/Hume/HumeClient.cs @@ -18,7 +18,7 @@ public HumeClient(string? apiKey = null, ClientOptions? clientOptions = null) { "X-Fern-Language", "C#" }, { "X-Fern-SDK-Name", "Hume" }, { "X-Fern-SDK-Version", Version.Current }, - { "User-Agent", "Hume/0.2.6" }, + { "User-Agent", "Hume/0.2.7" }, } ); foreach (var header in platformHeaders) diff --git a/src/Hume/Tts/ITtsClient.cs b/src/Hume/Tts/ITtsClient.cs index 9fb215c..09fb76d 100644 --- a/src/Hume/Tts/ITtsClient.cs +++ b/src/Hume/Tts/ITtsClient.cs @@ -23,7 +23,7 @@ WithRawResponseTask SynthesizeJsonAsync( /// /// The response contains the generated audio file in the requested format. /// - WithRawResponseTask SynthesizeFileAsync( + WithRawResponseTask SynthesizeFileAsync( PostedTts request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -32,7 +32,7 @@ WithRawResponseTask SynthesizeJsonAsync( /// /// Streams synthesized speech using the specified voice. If no voice is provided, a novel voice will be generated dynamically. Optionally, additional context can be included to influence the speech's style and prosody. /// - WithRawResponseTask SynthesizeFileStreamingAsync( + WithRawResponseTask SynthesizeFileStreamingAsync( PostedTts request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -49,7 +49,7 @@ IAsyncEnumerable> SynthesizeJsonStrea CancellationToken cancellationToken = default ); - WithRawResponseTask ConvertVoiceFileAsync( + WithRawResponseTask ConvertVoiceFileAsync( ConvertVoiceFileRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -60,4 +60,8 @@ IAsyncEnumerable> ConvertVoiceJsonAsy RequestOptions? options = null, CancellationToken cancellationToken = default ); + + IStreamInputApi CreateStreamInputApi(); + + IStreamInputApi CreateStreamInputApi(StreamInputApi.Options options); } diff --git a/src/Hume/Tts/StreamInput/IStreamInputApi.cs b/src/Hume/Tts/StreamInput/IStreamInputApi.cs new file mode 100644 index 0000000..1c61585 --- /dev/null +++ b/src/Hume/Tts/StreamInput/IStreamInputApi.cs @@ -0,0 +1,20 @@ +using Hume.Core.WebSockets; + +namespace Hume.Tts; + +public partial interface IStreamInputApi : IAsyncDisposable, IDisposable +{ + public Event Connected { get; } + public Event Closed { get; } + public Event ExceptionOccurred { get; } + public Event Reconnecting { get; } + public ConnectionStatus Status { get; } + global::System.Threading.Tasks.Task ConnectAsync(CancellationToken cancellationToken = default); + + global::System.Threading.Tasks.Task Send( + PublishTts message, + CancellationToken cancellationToken = default + ); + + global::System.Threading.Tasks.Task CloseAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Hume/Tts/StreamInput/StreamInputApi.cs b/src/Hume/Tts/StreamInput/StreamInputApi.cs index 09eaaa0..069b821 100644 --- a/src/Hume/Tts/StreamInput/StreamInputApi.cs +++ b/src/Hume/Tts/StreamInput/StreamInputApi.cs @@ -1,5 +1,5 @@ -using System.ComponentModel; -using System.Text.Json; +using global::System.ComponentModel; +using global::System.Text.Json; using Hume.Core; using Hume.Core.WebSockets; @@ -8,7 +8,11 @@ namespace Hume.Tts; /// /// Generate emotionally expressive speech. /// -public partial class StreamInputApi : IAsyncDisposable, IDisposable, INotifyPropertyChanged +public partial class StreamInputApi + : IStreamInputApi, + IAsyncDisposable, + IDisposable, + INotifyPropertyChanged { private readonly StreamInputApi.Options _options; @@ -35,6 +39,12 @@ public event PropertyChangedEventHandler PropertyChanged /// public readonly Event TimestampMessage = new(); + /// + /// Event handler for unknown/unrecognized message types. + /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. + /// + public readonly Event UnknownMessage = new(); + /// /// Default constructor /// @@ -62,6 +72,12 @@ public StreamInputApi(StreamInputApi.Options options) }; uri.Path = $"{uri.Path.TrimEnd('/')}/stream/input"; _client = new WebSocketClient(uri.Uri, OnTextMessage); + _client.HttpInvoker = _options.HttpInvoker; + _client.IsReconnectionEnabled = _options.IsReconnectionEnabled; + _client.ReconnectTimeout = _options.ReconnectTimeout; + _client.ErrorReconnectTimeout = _options.ErrorReconnectTimeout; + _client.LostReconnectTimeout = _options.LostReconnectTimeout; + _client.Backoff = _options.ReconnectBackoff; } /// @@ -84,6 +100,11 @@ public StreamInputApi(StreamInputApi.Options options) /// public Event ExceptionOccurred => _client.ExceptionOccurred; + /// + /// Event raised when the WebSocket connection is re-established after a disconnect. + /// + public Event Reconnecting => _client.Reconnecting; + /// /// Disposes of event subscriptions /// @@ -91,14 +112,15 @@ private void DisposeEvents() { SnippetAudioChunk.Dispose(); TimestampMessage.Dispose(); + UnknownMessage.Dispose(); } /// /// Dispatches incoming WebSocket messages /// - private async System.Threading.Tasks.Task OnTextMessage(Stream stream) + private async global::System.Threading.Tasks.Task OnTextMessage(Stream stream) { - var json = await JsonSerializer.DeserializeAsync(stream); + using var json = await JsonSerializer.DeserializeAsync(stream); if (json == null) { await ExceptionOccurred @@ -124,25 +146,54 @@ await ExceptionOccurred } } - await ExceptionOccurred - .RaiseEvent(new Exception($"Unknown message: {json.ToString()}")) + await UnknownMessage.RaiseEvent(json.RootElement.Clone()).ConfigureAwait(false); + } + + /// + /// Serializes and sends a JSON message to the server + /// + private async global::System.Threading.Tasks.Task SendJsonAsync( + object message, + CancellationToken cancellationToken = default + ) + { + await _client + .SendInstant(JsonUtils.Serialize(message), cancellationToken) .ConfigureAwait(false); } + /// + /// Injects a fake text message for testing. Dispatches through the normal message handling pipeline. + /// + internal async global::System.Threading.Tasks.Task InjectTestMessage(string rawJson) + { + using var stream = new MemoryStream(global::System.Text.Encoding.UTF8.GetBytes(rawJson)); + await OnTextMessage(stream).ConfigureAwait(false); + } + /// /// Asynchronously establishes a WebSocket connection. /// - public async System.Threading.Tasks.Task ConnectAsync() + public async global::System.Threading.Tasks.Task ConnectAsync( + CancellationToken cancellationToken = default + ) { - await _client.ConnectAsync().ConfigureAwait(false); +#if NET6_0_OR_GREATER + _client.DeflateOptions = _options.EnableCompression + ? new System.Net.WebSockets.WebSocketDeflateOptions() + : null; +#endif + await _client.ConnectAsync(cancellationToken).ConfigureAwait(false); } /// /// Asynchronously closes the WebSocket connection. /// - public async System.Threading.Tasks.Task CloseAsync() + public async global::System.Threading.Tasks.Task CloseAsync( + CancellationToken cancellationToken = default + ) { - await _client.CloseAsync().ConfigureAwait(false); + await _client.CloseAsync(cancellationToken).ConfigureAwait(false); } /// @@ -168,9 +219,12 @@ public void Dispose() /// /// Sends a PublishTts message to the server /// - public async System.Threading.Tasks.Task Send(PublishTts message) + public async global::System.Threading.Tasks.Task Send( + PublishTts message, + CancellationToken cancellationToken = default + ) { - await _client.SendInstant(JsonUtils.Serialize(message)).ConfigureAwait(false); + await SendJsonAsync(message, cancellationToken).ConfigureAwait(false); } /// @@ -183,6 +237,17 @@ public class Options /// public string BaseUrl { get; set; } = "wss://api.hume.ai/v0/tts"; + /// + /// Enable per-message deflate compression (RFC 7692). When true, the client sets ClientWebSocketOptions.DangerousDeflateOptions before connecting. Compression is negotiated during the handshake; if the server does not support it, the connection proceeds uncompressed. Default: false. + /// Security warning: do not enable compression when transmitting data containing secrets — compressed encrypted payloads are vulnerable to CRIME/BREACH side-channel attacks. See ClientWebSocketOptions.DangerousDeflateOptions for details. + /// + public bool EnableCompression { get; set; } = false; + + /// + /// Optional HTTP/2 handler for multiplexed WebSocket connections (.NET 7+). + /// + public System.Net.Http.HttpMessageInvoker? HttpInvoker { get; set; } + /// /// Access token used for authenticating the client. If not provided, an `api_key` must be provided to authenticate. /// @@ -233,5 +298,30 @@ public class Options /// For more details, refer to the [Authentication Strategies Guide](/docs/introduction/api-key#authentication-strategies). /// public string? ApiKey { get; set; } + + /// + /// Enable or disable automatic reconnection. Default: false. + /// + public bool IsReconnectionEnabled { get; set; } = false; + + /// + /// Time to wait before reconnecting if no message comes from the server. Set null to disable. Default: 1 minute. + /// + public TimeSpan? ReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the last reconnection attempt failed. Set null to disable. Default: 1 minute. + /// + public TimeSpan? ErrorReconnectTimeout { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Time to wait before reconnecting if the connection is lost with a transient error. Set null to disable (reconnect immediately). Default: null. + /// + public TimeSpan? LostReconnectTimeout { get; set; } + + /// + /// Backoff strategy for reconnection delays. Controls interval growth, jitter, and max attempts. Set to null to use fixed-interval reconnection (legacy behavior). Default: exponential backoff, 1s→60s, unlimited attempts, with jitter. + /// + public ReconnectStrategy? ReconnectBackoff { get; set; } = new ReconnectStrategy(); } } diff --git a/src/Hume/Tts/TtsClient.cs b/src/Hume/Tts/TtsClient.cs index 37e61f2..c91b1e1 100644 --- a/src/Hume/Tts/TtsClient.cs +++ b/src/Hume/Tts/TtsClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; using OneOf; @@ -7,7 +7,7 @@ namespace Hume.Tts; public partial class TtsClient : ITtsClient { - private RawClient _client; + private readonly RawClient _client; internal TtsClient(RawClient client) { @@ -17,7 +17,9 @@ internal TtsClient(RawClient client) public IVoicesClient Voices { get; } - private async System.Threading.Tasks.Task> SynthesizeJsonAsyncCore( + private async global::System.Threading.Tasks.Task< + WithRawResponse + > SynthesizeJsonAsyncCore( PostedTts request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -46,7 +48,9 @@ private async System.Threading.Tasks.Task> Synthesize .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -72,7 +76,9 @@ private async System.Threading.Tasks.Task> Synthesize } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -95,8 +101,8 @@ private async System.Threading.Tasks.Task> Synthesize } } - private async System.Threading.Tasks.Task< - WithRawResponse + private async global::System.Threading.Tasks.Task< + WithRawResponse > SynthesizeFileAsyncCore( PostedTts request, RequestOptions? options = null, @@ -127,7 +133,7 @@ private async System.Threading.Tasks.Task< if (response.StatusCode is >= 200 and < 400) { var stream = await response.Raw.Content.ReadAsStreamAsync(); - return new WithRawResponse() + return new WithRawResponse() { Data = stream, RawResponse = new RawResponse() @@ -139,7 +145,9 @@ private async System.Threading.Tasks.Task< }; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -162,8 +170,8 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< - WithRawResponse + private async global::System.Threading.Tasks.Task< + WithRawResponse > SynthesizeFileStreamingAsyncCore( PostedTts request, RequestOptions? options = null, @@ -194,7 +202,7 @@ private async System.Threading.Tasks.Task< if (response.StatusCode is >= 200 and < 400) { var stream = await response.Raw.Content.ReadAsStreamAsync(); - return new WithRawResponse() + return new WithRawResponse() { Data = stream, RawResponse = new RawResponse() @@ -206,7 +214,9 @@ private async System.Threading.Tasks.Task< }; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -229,8 +239,8 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task< - WithRawResponse + private async global::System.Threading.Tasks.Task< + WithRawResponse > ConvertVoiceFileAsyncCore( ConvertVoiceFileRequest request, RequestOptions? options = null, @@ -266,7 +276,7 @@ private async System.Threading.Tasks.Task< if (response.StatusCode is >= 200 and < 400) { var stream = await response.Raw.Content.ReadAsStreamAsync(); - return new WithRawResponse() + return new WithRawResponse() { Data = stream, RawResponse = new RawResponse() @@ -278,7 +288,9 @@ private async System.Threading.Tasks.Task< }; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -322,7 +334,7 @@ private async System.Threading.Tasks.Task< /// }, /// }, /// }, - /// Format = new FormatMp3 { Type = "mp3" }, + /// Format = new FormatMp3(), /// NumGenerations = 1, /// Utterances = new List<PostedUtterance>() /// { @@ -358,7 +370,7 @@ public WithRawResponseTask SynthesizeJsonAsync( /// new PostedTts /// { /// Context = new PostedContextWithGenerationId { GenerationId = "" }, - /// Format = new FormatMp3 { Type = "mp3" }, + /// Format = new FormatMp3(), /// NumGenerations = 1, /// Utterances = new List<PostedUtterance>() /// { @@ -373,13 +385,13 @@ public WithRawResponseTask SynthesizeJsonAsync( /// } /// ); /// - public WithRawResponseTask SynthesizeFileAsync( + public WithRawResponseTask SynthesizeFileAsync( PostedTts request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { - return new WithRawResponseTask( + return new WithRawResponseTask( SynthesizeFileAsyncCore(request, options, cancellationToken) ); } @@ -407,13 +419,13 @@ public WithRawResponseTask SynthesizeJsonAsync( /// } /// ); /// - public WithRawResponseTask SynthesizeFileStreamingAsync( + public WithRawResponseTask SynthesizeFileStreamingAsync( PostedTts request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { - return new WithRawResponseTask( + return new WithRawResponseTask( SynthesizeFileStreamingAsyncCore(request, options, cancellationToken) ); } @@ -494,7 +506,9 @@ public async IAsyncEnumerable< yield break; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -517,13 +531,13 @@ public async IAsyncEnumerable< } } - public WithRawResponseTask ConvertVoiceFileAsync( + public WithRawResponseTask ConvertVoiceFileAsync( ConvertVoiceFileRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default ) { - return new WithRawResponseTask( + return new WithRawResponseTask( ConvertVoiceFileAsyncCore(request, options, cancellationToken) ); } @@ -585,7 +599,9 @@ public async IAsyncEnumerable> Conver yield break; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -608,12 +624,14 @@ public async IAsyncEnumerable> Conver } } - public StreamInputApi CreateStreamInputApi() + public IStreamInputApi CreateStreamInputApi() { - return new StreamInputApi(new StreamInputApi.Options()); + return new StreamInputApi( + new StreamInputApi.Options { BaseUrl = _client.Options.Environment.Tts } + ); } - public StreamInputApi CreateStreamInputApi(StreamInputApi.Options options) + public IStreamInputApi CreateStreamInputApi(StreamInputApi.Options options) { return new StreamInputApi(options); } diff --git a/src/Hume/Tts/Types/AudioEncoding.cs b/src/Hume/Tts/Types/AudioEncoding.cs index 10e956d..21cd692 100644 --- a/src/Hume/Tts/Types/AudioEncoding.cs +++ b/src/Hume/Tts/Types/AudioEncoding.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/AudioFormatType.cs b/src/Hume/Tts/Types/AudioFormatType.cs index b08361d..6f711fc 100644 --- a/src/Hume/Tts/Types/AudioFormatType.cs +++ b/src/Hume/Tts/Types/AudioFormatType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.Tts; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(AudioFormatType.AudioFormatTypeSerializer))] [Serializable] public readonly record struct AudioFormatType : IStringEnum { @@ -54,6 +55,55 @@ public override string ToString() public static explicit operator AudioFormatType(string value) => new(value); + internal class AudioFormatTypeSerializer : JsonConverter + { + public override AudioFormatType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new AudioFormatType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + AudioFormatType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override AudioFormatType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new AudioFormatType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + AudioFormatType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/Tts/Types/ErrorResponse.cs b/src/Hume/Tts/Types/ErrorResponse.cs index 56193ca..35862f3 100644 --- a/src/Hume/Tts/Types/ErrorResponse.cs +++ b/src/Hume/Tts/Types/ErrorResponse.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/FormatMp3.cs b/src/Hume/Tts/Types/FormatMp3.cs index a1f26f9..9e0b472 100644 --- a/src/Hume/Tts/Types/FormatMp3.cs +++ b/src/Hume/Tts/Types/FormatMp3.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -12,12 +12,15 @@ public record FormatMp3 : IJsonOnDeserialized private readonly IDictionary _extensionData = new Dictionary(); + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "mp3"; - set => value.Assert(value == "mp3", string.Format("'Type' must be {0}", "mp3")); - } + public FormatMp3.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -30,4 +33,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "mp3"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/Tts/Types/FormatPcm.cs b/src/Hume/Tts/Types/FormatPcm.cs index 170542f..31b21d6 100644 --- a/src/Hume/Tts/Types/FormatPcm.cs +++ b/src/Hume/Tts/Types/FormatPcm.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -12,12 +12,15 @@ public record FormatPcm : IJsonOnDeserialized private readonly IDictionary _extensionData = new Dictionary(); + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "pcm"; - set => value.Assert(value == "pcm", string.Format("'Type' must be {0}", "pcm")); - } + public FormatPcm.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -30,4 +33,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "pcm"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/Tts/Types/FormatWav.cs b/src/Hume/Tts/Types/FormatWav.cs index 9b3fc1e..dc1a43d 100644 --- a/src/Hume/Tts/Types/FormatWav.cs +++ b/src/Hume/Tts/Types/FormatWav.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -12,12 +12,15 @@ public record FormatWav : IJsonOnDeserialized private readonly IDictionary _extensionData = new Dictionary(); + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "wav"; - set => value.Assert(value == "wav", string.Format("'Type' must be {0}", "wav")); - } + public FormatWav.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -30,4 +33,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "wav"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/Tts/Types/HttpValidationError.cs b/src/Hume/Tts/Types/HttpValidationError.cs index b8e5c80..893a43b 100644 --- a/src/Hume/Tts/Types/HttpValidationError.cs +++ b/src/Hume/Tts/Types/HttpValidationError.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/MillisecondInterval.cs b/src/Hume/Tts/Types/MillisecondInterval.cs index 3c892e7..9e3be6c 100644 --- a/src/Hume/Tts/Types/MillisecondInterval.cs +++ b/src/Hume/Tts/Types/MillisecondInterval.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/OctaveVersion.cs b/src/Hume/Tts/Types/OctaveVersion.cs index d57e906..80d76d6 100644 --- a/src/Hume/Tts/Types/OctaveVersion.cs +++ b/src/Hume/Tts/Types/OctaveVersion.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.Tts; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(OctaveVersion.OctaveVersionSerializer))] [Serializable] public readonly record struct OctaveVersion : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator OctaveVersion(string value) => new(value); + internal class OctaveVersionSerializer : JsonConverter + { + public override OctaveVersion Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new OctaveVersion(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + OctaveVersion value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override OctaveVersion ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new OctaveVersion(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + OctaveVersion value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/Tts/Types/PostedContextWithGenerationId.cs b/src/Hume/Tts/Types/PostedContextWithGenerationId.cs index a3a4676..366c549 100644 --- a/src/Hume/Tts/Types/PostedContextWithGenerationId.cs +++ b/src/Hume/Tts/Types/PostedContextWithGenerationId.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/PostedContextWithUtterances.cs b/src/Hume/Tts/Types/PostedContextWithUtterances.cs index e8abff0..40caefa 100644 --- a/src/Hume/Tts/Types/PostedContextWithUtterances.cs +++ b/src/Hume/Tts/Types/PostedContextWithUtterances.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/PostedTts.cs b/src/Hume/Tts/Types/PostedTts.cs index f295189..a7381c6 100644 --- a/src/Hume/Tts/Types/PostedTts.cs +++ b/src/Hume/Tts/Types/PostedTts.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/Tts/Types/PostedUtterance.cs b/src/Hume/Tts/Types/PostedUtterance.cs index 6ccaa7d..beb75d4 100644 --- a/src/Hume/Tts/Types/PostedUtterance.cs +++ b/src/Hume/Tts/Types/PostedUtterance.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/Tts/Types/PostedUtteranceVoiceWithId.cs b/src/Hume/Tts/Types/PostedUtteranceVoiceWithId.cs index 6db0699..190d4a5 100644 --- a/src/Hume/Tts/Types/PostedUtteranceVoiceWithId.cs +++ b/src/Hume/Tts/Types/PostedUtteranceVoiceWithId.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/PostedUtteranceVoiceWithName.cs b/src/Hume/Tts/Types/PostedUtteranceVoiceWithName.cs index a59c52b..57dd41e 100644 --- a/src/Hume/Tts/Types/PostedUtteranceVoiceWithName.cs +++ b/src/Hume/Tts/Types/PostedUtteranceVoiceWithName.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/PublishTts.cs b/src/Hume/Tts/Types/PublishTts.cs index e8897fb..3187c89 100644 --- a/src/Hume/Tts/Types/PublishTts.cs +++ b/src/Hume/Tts/Types/PublishTts.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/Tts/Types/ReturnGeneration.cs b/src/Hume/Tts/Types/ReturnGeneration.cs index 04d8225..a8e0733 100644 --- a/src/Hume/Tts/Types/ReturnGeneration.cs +++ b/src/Hume/Tts/Types/ReturnGeneration.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/ReturnPagedVoices.cs b/src/Hume/Tts/Types/ReturnPagedVoices.cs index 89d7f2e..0c9b5f3 100644 --- a/src/Hume/Tts/Types/ReturnPagedVoices.cs +++ b/src/Hume/Tts/Types/ReturnPagedVoices.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/ReturnTts.cs b/src/Hume/Tts/Types/ReturnTts.cs index 1adeb75..e67df6f 100644 --- a/src/Hume/Tts/Types/ReturnTts.cs +++ b/src/Hume/Tts/Types/ReturnTts.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/ReturnVoice.cs b/src/Hume/Tts/Types/ReturnVoice.cs index e0679df..ae2bf4d 100644 --- a/src/Hume/Tts/Types/ReturnVoice.cs +++ b/src/Hume/Tts/Types/ReturnVoice.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/Snippet.cs b/src/Hume/Tts/Types/Snippet.cs index 647c527..77e01f4 100644 --- a/src/Hume/Tts/Types/Snippet.cs +++ b/src/Hume/Tts/Types/Snippet.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/SnippetAudioChunk.cs b/src/Hume/Tts/Types/SnippetAudioChunk.cs index 459a893..7bea328 100644 --- a/src/Hume/Tts/Types/SnippetAudioChunk.cs +++ b/src/Hume/Tts/Types/SnippetAudioChunk.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -72,12 +72,15 @@ public record SnippetAudioChunk : IJsonOnDeserialized [JsonPropertyName("transcribed_text")] public string? TranscribedText { get; set; } + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "audio"; - set => value.Assert(value == "audio", string.Format("'Type' must be {0}", "audio")); - } + public SnippetAudioChunk.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); /// /// The index of the utterance in the request that the parent snippet of this chunk corresponds to. @@ -96,4 +99,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "audio"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/Tts/Types/Timestamp.cs b/src/Hume/Tts/Types/Timestamp.cs index b67f544..45e2b8b 100644 --- a/src/Hume/Tts/Types/Timestamp.cs +++ b/src/Hume/Tts/Types/Timestamp.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; diff --git a/src/Hume/Tts/Types/TimestampMessage.cs b/src/Hume/Tts/Types/TimestampMessage.cs index 832d724..99b642a 100644 --- a/src/Hume/Tts/Types/TimestampMessage.cs +++ b/src/Hume/Tts/Types/TimestampMessage.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; @@ -39,12 +39,15 @@ public record TimestampMessage : IJsonOnDeserialized [JsonPropertyName("timestamp")] public required Timestamp Timestamp { get; set; } + [JsonRequired] [JsonPropertyName("type")] - public string Type - { - get => "timestamp"; - set => value.Assert(value == "timestamp", string.Format("'Type' must be {0}", "timestamp")); - } + public TimestampMessage.TypeLiteral Type { get; +#if NET5_0_OR_GREATER + init; +#else + set; +#endif + } = new(); [JsonIgnore] public ReadOnlyAdditionalProperties AdditionalProperties { get; private set; } = new(); @@ -57,4 +60,78 @@ public override string ToString() { return JsonUtils.Serialize(this); } + + [JsonConverter(typeof(TypeLiteralConverter))] + public readonly struct TypeLiteral + { + public const string Value = "timestamp"; + + public static implicit operator string(TypeLiteral _) => Value; + + public override string ToString() => Value; + + public override int GetHashCode() => + global::System.StringComparer.Ordinal.GetHashCode(Value); + + public override bool Equals(object? obj) => obj is TypeLiteral; + + public static bool operator ==(TypeLiteral _, TypeLiteral __) => true; + + public static bool operator !=(TypeLiteral _, TypeLiteral __) => false; + + internal sealed class TypeLiteralConverter : JsonConverter + { + public override TypeLiteral Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void Write( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WriteStringValue(TypeLiteral.Value); + + public override TypeLiteral ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var value = reader.GetString(); + if (value != TypeLiteral.Value) + { + throw new JsonException( + "Expected \"" + + TypeLiteral.Value + + "\" for type discriminator but got \"" + + value + + "\"." + ); + } + return new TypeLiteral(); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TypeLiteral value, + JsonSerializerOptions options + ) => writer.WritePropertyName(TypeLiteral.Value); + } + } } diff --git a/src/Hume/Tts/Types/TimestampType.cs b/src/Hume/Tts/Types/TimestampType.cs index def2fcf..a453f83 100644 --- a/src/Hume/Tts/Types/TimestampType.cs +++ b/src/Hume/Tts/Types/TimestampType.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.Tts; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(TimestampType.TimestampTypeSerializer))] [Serializable] public readonly record struct TimestampType : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator TimestampType(string value) => new(value); + internal class TimestampTypeSerializer : JsonConverter + { + public override TimestampType Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new TimestampType(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + TimestampType value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override TimestampType ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new TimestampType(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + TimestampType value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/Tts/Types/ValidationError.cs b/src/Hume/Tts/Types/ValidationError.cs index 13dd2f0..f8f8502 100644 --- a/src/Hume/Tts/Types/ValidationError.cs +++ b/src/Hume/Tts/Types/ValidationError.cs @@ -1,5 +1,5 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume; using Hume.Core; using OneOf; diff --git a/src/Hume/Tts/Types/VoiceProvider.cs b/src/Hume/Tts/Types/VoiceProvider.cs index 99a51c5..9db80f8 100644 --- a/src/Hume/Tts/Types/VoiceProvider.cs +++ b/src/Hume/Tts/Types/VoiceProvider.cs @@ -1,9 +1,10 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.Tts; -[JsonConverter(typeof(StringEnumSerializer))] +[JsonConverter(typeof(VoiceProvider.VoiceProviderSerializer))] [Serializable] public readonly record struct VoiceProvider : IStringEnum { @@ -52,6 +53,55 @@ public override string ToString() public static explicit operator VoiceProvider(string value) => new(value); + internal class VoiceProviderSerializer : JsonConverter + { + public override VoiceProvider Read( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON value could not be read as a string." + ); + return new VoiceProvider(stringValue); + } + + public override void Write( + Utf8JsonWriter writer, + VoiceProvider value, + JsonSerializerOptions options + ) + { + writer.WriteStringValue(value.Value); + } + + public override VoiceProvider ReadAsPropertyName( + ref Utf8JsonReader reader, + global::System.Type typeToConvert, + JsonSerializerOptions options + ) + { + var stringValue = + reader.GetString() + ?? throw new global::System.Exception( + "The JSON property name could not be read as a string." + ); + return new VoiceProvider(stringValue); + } + + public override void WriteAsPropertyName( + Utf8JsonWriter writer, + VoiceProvider value, + JsonSerializerOptions options + ) + { + writer.WritePropertyName(value.Value); + } + } + /// /// Constant strings for enum values /// diff --git a/src/Hume/Tts/Voices/IVoicesClient.cs b/src/Hume/Tts/Voices/IVoicesClient.cs index 48b8da0..2f70558 100644 --- a/src/Hume/Tts/Voices/IVoicesClient.cs +++ b/src/Hume/Tts/Voices/IVoicesClient.cs @@ -8,7 +8,7 @@ public partial interface IVoicesClient /// /// Lists voices you have saved in your account, or voices from the [Voice Library](https://app.hume.ai/tts/voice-library). /// - System.Threading.Tasks.Task> ListAsync( + global::System.Threading.Tasks.Task> ListAsync( VoicesListRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -28,7 +28,7 @@ WithRawResponseTask CreateAsync( /// /// Deletes a previously generated custom voice. /// - System.Threading.Tasks.Task DeleteAsync( + global::System.Threading.Tasks.Task DeleteAsync( VoicesDeleteRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default diff --git a/src/Hume/Tts/Voices/Requests/PostedVoice.cs b/src/Hume/Tts/Voices/Requests/PostedVoice.cs index eacc3ba..6871e16 100644 --- a/src/Hume/Tts/Voices/Requests/PostedVoice.cs +++ b/src/Hume/Tts/Voices/Requests/PostedVoice.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.Tts; diff --git a/src/Hume/Tts/Voices/Requests/VoicesDeleteRequest.cs b/src/Hume/Tts/Voices/Requests/VoicesDeleteRequest.cs index cbe6233..987f701 100644 --- a/src/Hume/Tts/Voices/Requests/VoicesDeleteRequest.cs +++ b/src/Hume/Tts/Voices/Requests/VoicesDeleteRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.Tts; diff --git a/src/Hume/Tts/Voices/Requests/VoicesListRequest.cs b/src/Hume/Tts/Voices/Requests/VoicesListRequest.cs index 6b1c547..d7c7cce 100644 --- a/src/Hume/Tts/Voices/Requests/VoicesListRequest.cs +++ b/src/Hume/Tts/Voices/Requests/VoicesListRequest.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using global::System.Text.Json.Serialization; using Hume.Core; namespace Hume.Tts; @@ -34,6 +34,13 @@ public record VoicesListRequest [JsonIgnore] public bool? AscendingOrder { get; set; } + /// + /// Filter voices by tag using `TAG:TAG_VALUE` syntax. + /// + /// For example, `GENDER:Male` returns only voices with the `GENDER` tag set to `Male`. Tag types are case-insensitive; tag values are case-sensitive. + /// + /// Multiple values are ANDed together; for example, `[GENDER:Male, LANGUAGE:Japanese]` returns only voices matching both criteria. + /// [JsonIgnore] public IEnumerable FilterTag { get; set; } = new List(); diff --git a/src/Hume/Tts/Voices/VoicesClient.cs b/src/Hume/Tts/Voices/VoicesClient.cs index 22cefc3..36cea39 100644 --- a/src/Hume/Tts/Voices/VoicesClient.cs +++ b/src/Hume/Tts/Voices/VoicesClient.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using global::System.Text.Json; using Hume; using Hume.Core; @@ -6,7 +6,7 @@ namespace Hume.Tts; public partial class VoicesClient : IVoicesClient { - private RawClient _client; + private readonly RawClient _client; internal VoicesClient(RawClient client) { @@ -27,7 +27,7 @@ private WithRawResponseTask ListInternalAsync( ); } - private async System.Threading.Tasks.Task< + private async global::System.Threading.Tasks.Task< WithRawResponse > ListInternalAsyncCore( VoicesListRequest request, @@ -65,7 +65,9 @@ private async System.Threading.Tasks.Task< .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -91,7 +93,9 @@ private async System.Threading.Tasks.Task< } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -114,7 +118,7 @@ private async System.Threading.Tasks.Task< } } - private async System.Threading.Tasks.Task> CreateAsyncCore( + private async global::System.Threading.Tasks.Task> CreateAsyncCore( PostedVoice request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -143,7 +147,9 @@ private async System.Threading.Tasks.Task> CreateAs .ConfigureAwait(false); if (response.StatusCode is >= 200 and < 400) { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { var responseData = JsonUtils.Deserialize(responseBody)!; @@ -169,7 +175,9 @@ private async System.Threading.Tasks.Task> CreateAs } } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) @@ -200,7 +208,7 @@ private async System.Threading.Tasks.Task> CreateAs /// new VoicesListRequest { Provider = Hume.Tts.VoiceProvider.CustomVoice } /// ); /// - public async System.Threading.Tasks.Task> ListAsync( + public async global::System.Threading.Tasks.Task> ListAsync( VoicesListRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -219,7 +227,7 @@ public async System.Threading.Tasks.Task> ListAsync( request, options, async (request, options, cancellationToken) => - await ListInternalAsync(request, options, cancellationToken), + await ListInternalAsync(request, options, cancellationToken).WithRawResponse(), request => request.PageNumber ?? 0, (request, offset) => { @@ -259,7 +267,7 @@ public WithRawResponseTask CreateAsync( /// /// await client.Tts.Voices.DeleteAsync(new VoicesDeleteRequest { Name = "David Hume" }); /// - public async System.Threading.Tasks.Task DeleteAsync( + public async global::System.Threading.Tasks.Task DeleteAsync( VoicesDeleteRequest request, RequestOptions? options = null, CancellationToken cancellationToken = default @@ -294,7 +302,9 @@ public async System.Threading.Tasks.Task DeleteAsync( return; } { - var responseBody = await response.Raw.Content.ReadAsStringAsync(); + var responseBody = await response + .Raw.Content.ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); try { switch (response.StatusCode) From 11d017b7de20d930846df84dd6ebd23adf6f1100 Mon Sep 17 00:00:00 2001 From: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:06:56 +0000 Subject: [PATCH 2/5] SDK regeneration --- .fern/metadata.json | 2 +- src/Hume/EmpathicVoice/Chat/ChatApi.cs | 146 +++++++++--------- src/Hume/EmpathicVoice/Chat/IChatApi.cs | 14 ++ .../ControlPlane/ControlPlaneApi.cs | 120 +++++++------- .../ControlPlane/IControlPlaneApi.cs | 14 ++ .../Stream/Stream/IStreamApi.cs | 5 + .../Stream/Stream/StreamApi.cs | 48 +++--- src/Hume/Tts/StreamInput/IStreamInputApi.cs | 4 + src/Hume/Tts/StreamInput/StreamInputApi.cs | 36 ++--- 9 files changed, 213 insertions(+), 176 deletions(-) diff --git a/.fern/metadata.json b/.fern/metadata.json index 868f30f..2eea317 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,7 +1,7 @@ { "cliVersion": "4.65.2", "generatorName": "fernapi/fern-csharp-sdk", - "generatorVersion": "2.59.5", + "generatorVersion": "2.59.6", "generatorConfig": { "namespace": "Hume", "client-class-name": "HumeClient", diff --git a/src/Hume/EmpathicVoice/Chat/ChatApi.cs b/src/Hume/EmpathicVoice/Chat/ChatApi.cs index 283d26b..5b5441d 100644 --- a/src/Hume/EmpathicVoice/Chat/ChatApi.cs +++ b/src/Hume/EmpathicVoice/Chat/ChatApi.cs @@ -24,143 +24,143 @@ public event PropertyChangedEventHandler PropertyChanged remove => _client.PropertyChanged -= value; } + /// + /// Default constructor + /// + public ChatApi() { } + + /// + /// Constructor with options + /// + public ChatApi(ChatApi.Options options) + { + _options = options; + var uri = new UriBuilder(_options.BaseUrl) + { + Query = new Hume.Core.QueryStringBuilder.Builder(capacity: 9) + .Add("access_token", _options.AccessToken) + .Add("allow_connection", _options.AllowConnection) + .Add("config_id", _options.ConfigId) + .Add("config_version", _options.ConfigVersion) + .Add("event_limit", _options.EventLimit) + .Add("resumed_chat_group_id", _options.ResumedChatGroupId) + .Add("verbose_transcription", _options.VerboseTranscription) + .Add("api_key", _options.ApiKey) + .AddDeepObject("session_settings", _options.SessionSettings) + .Build(), + }; + uri.Path = $"{uri.Path.TrimEnd('/')}/chat"; + _client = new WebSocketClient(uri.Uri, OnTextMessage); + _client.HttpInvoker = _options.HttpInvoker; + _client.IsReconnectionEnabled = _options.IsReconnectionEnabled; + _client.ReconnectTimeout = _options.ReconnectTimeout; + _client.ErrorReconnectTimeout = _options.ErrorReconnectTimeout; + _client.LostReconnectTimeout = _options.LostReconnectTimeout; + _client.Backoff = _options.ReconnectBackoff; + } + + /// + /// Gets the current connection status of the WebSocket. + /// + public ConnectionStatus Status => _client.Status; + + /// + /// Event that is raised when the WebSocket connection is established. + /// + public Event Connected => _client.Connected; + + /// + /// Event that is raised when the WebSocket connection is closed. + /// + public Event Closed => _client.Closed; + + /// + /// Event that is raised when an exception occurs during WebSocket operations. + /// + public Event ExceptionOccurred => _client.ExceptionOccurred; + + /// + /// Event raised when the WebSocket connection is re-established after a disconnect. + /// + public Event Reconnecting => _client.Reconnecting; + /// /// Event handler for AssistantEnd. /// Use AssistantEnd.Subscribe(...) to receive messages. /// - public readonly Event AssistantEnd = new(); + public Event AssistantEnd { get; } = new(); /// /// Event handler for AssistantMessage. /// Use AssistantMessage.Subscribe(...) to receive messages. /// - public readonly Event AssistantMessage = new(); + public Event AssistantMessage { get; } = new(); /// /// Event handler for AssistantProsody. /// Use AssistantProsody.Subscribe(...) to receive messages. /// - public readonly Event AssistantProsody = new(); + public Event AssistantProsody { get; } = new(); /// /// Event handler for AudioOutput. /// Use AudioOutput.Subscribe(...) to receive messages. /// - public readonly Event AudioOutput = new(); + public Event AudioOutput { get; } = new(); /// /// Event handler for ChatMetadata. /// Use ChatMetadata.Subscribe(...) to receive messages. /// - public readonly Event ChatMetadata = new(); + public Event ChatMetadata { get; } = new(); /// /// Event handler for WebSocketError. /// Use WebSocketError.Subscribe(...) to receive messages. /// - public readonly Event WebSocketError = new(); + public Event WebSocketError { get; } = new(); /// /// Event handler for UserInterruption. /// Use UserInterruption.Subscribe(...) to receive messages. /// - public readonly Event UserInterruption = new(); + public Event UserInterruption { get; } = new(); /// /// Event handler for UserMessage. /// Use UserMessage.Subscribe(...) to receive messages. /// - public readonly Event UserMessage = new(); + public Event UserMessage { get; } = new(); /// /// Event handler for ToolCallMessage. /// Use ToolCallMessage.Subscribe(...) to receive messages. /// - public readonly Event ToolCallMessage = new(); + public Event ToolCallMessage { get; } = new(); /// /// Event handler for ToolResponseMessage. /// Use ToolResponseMessage.Subscribe(...) to receive messages. /// - public readonly Event ToolResponseMessage = new(); + public Event ToolResponseMessage { get; } = new(); /// /// Event handler for ToolErrorMessage. /// Use ToolErrorMessage.Subscribe(...) to receive messages. /// - public readonly Event ToolErrorMessage = new(); + public Event ToolErrorMessage { get; } = new(); /// /// Event handler for SessionSettings. /// Use SessionSettings.Subscribe(...) to receive messages. /// - public readonly Event SessionSettings = new(); + public Event SessionSettings { get; } = new(); /// /// Event handler for unknown/unrecognized message types. /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. /// - public readonly Event UnknownMessage = new(); - - /// - /// Default constructor - /// - public ChatApi() { } - - /// - /// Constructor with options - /// - public ChatApi(ChatApi.Options options) - { - _options = options; - var uri = new UriBuilder(_options.BaseUrl) - { - Query = new Hume.Core.QueryStringBuilder.Builder(capacity: 9) - .Add("access_token", _options.AccessToken) - .Add("allow_connection", _options.AllowConnection) - .Add("config_id", _options.ConfigId) - .Add("config_version", _options.ConfigVersion) - .Add("event_limit", _options.EventLimit) - .Add("resumed_chat_group_id", _options.ResumedChatGroupId) - .Add("verbose_transcription", _options.VerboseTranscription) - .Add("api_key", _options.ApiKey) - .AddDeepObject("session_settings", _options.SessionSettings) - .Build(), - }; - uri.Path = $"{uri.Path.TrimEnd('/')}/chat"; - _client = new WebSocketClient(uri.Uri, OnTextMessage); - _client.HttpInvoker = _options.HttpInvoker; - _client.IsReconnectionEnabled = _options.IsReconnectionEnabled; - _client.ReconnectTimeout = _options.ReconnectTimeout; - _client.ErrorReconnectTimeout = _options.ErrorReconnectTimeout; - _client.LostReconnectTimeout = _options.LostReconnectTimeout; - _client.Backoff = _options.ReconnectBackoff; - } - - /// - /// Gets the current connection status of the WebSocket. - /// - public ConnectionStatus Status => _client.Status; - - /// - /// Event that is raised when the WebSocket connection is established. - /// - public Event Connected => _client.Connected; - - /// - /// Event that is raised when the WebSocket connection is closed. - /// - public Event Closed => _client.Closed; - - /// - /// Event that is raised when an exception occurs during WebSocket operations. - /// - public Event ExceptionOccurred => _client.ExceptionOccurred; - - /// - /// Event raised when the WebSocket connection is re-established after a disconnect. - /// - public Event Reconnecting => _client.Reconnecting; + public Event UnknownMessage { get; } = new(); /// /// Disposes of event subscriptions diff --git a/src/Hume/EmpathicVoice/Chat/IChatApi.cs b/src/Hume/EmpathicVoice/Chat/IChatApi.cs index 28a6a33..36e5317 100644 --- a/src/Hume/EmpathicVoice/Chat/IChatApi.cs +++ b/src/Hume/EmpathicVoice/Chat/IChatApi.cs @@ -1,3 +1,4 @@ +using global::System.Text.Json; using Hume.Core.WebSockets; using OneOf; @@ -9,6 +10,19 @@ public partial interface IChatApi : IAsyncDisposable, IDisposable public Event Closed { get; } public Event ExceptionOccurred { get; } public Event Reconnecting { get; } + public Event AssistantEnd { get; } + public Event AssistantMessage { get; } + public Event AssistantProsody { get; } + public Event AudioOutput { get; } + public Event ChatMetadata { get; } + public Event WebSocketError { get; } + public Event UserInterruption { get; } + public Event UserMessage { get; } + public Event ToolCallMessage { get; } + public Event ToolResponseMessage { get; } + public Event ToolErrorMessage { get; } + public Event SessionSettings { get; } + public Event UnknownMessage { get; } public ConnectionStatus Status { get; } global::System.Threading.Tasks.Task ConnectAsync(CancellationToken cancellationToken = default); diff --git a/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneApi.cs b/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneApi.cs index 3efe56d..cf50661 100644 --- a/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneApi.cs +++ b/src/Hume/EmpathicVoice/ControlPlane/ControlPlaneApi.cs @@ -28,130 +28,130 @@ public event PropertyChangedEventHandler PropertyChanged remove => _client.PropertyChanged -= value; } + /// + /// Constructor with options + /// + public ControlPlaneApi(ControlPlaneApi.Options options) + { + _options = options; + var uri = new UriBuilder(_options.BaseUrl) + { + Query = new Hume.Core.QueryStringBuilder.Builder(capacity: 1) + .Add("access_token", _options.AccessToken) + .Build(), + }; + uri.Path = $"{uri.Path.TrimEnd('/')}/chat/{Uri.EscapeDataString(_options.ChatId)}/connect"; + _client = new WebSocketClient(uri.Uri, OnTextMessage); + _client.HttpInvoker = _options.HttpInvoker; + _client.IsReconnectionEnabled = _options.IsReconnectionEnabled; + _client.ReconnectTimeout = _options.ReconnectTimeout; + _client.ErrorReconnectTimeout = _options.ErrorReconnectTimeout; + _client.LostReconnectTimeout = _options.LostReconnectTimeout; + _client.Backoff = _options.ReconnectBackoff; + } + + /// + /// Gets the current connection status of the WebSocket. + /// + public ConnectionStatus Status => _client.Status; + + /// + /// Event that is raised when the WebSocket connection is established. + /// + public Event Connected => _client.Connected; + + /// + /// Event that is raised when the WebSocket connection is closed. + /// + public Event Closed => _client.Closed; + + /// + /// Event that is raised when an exception occurs during WebSocket operations. + /// + public Event ExceptionOccurred => _client.ExceptionOccurred; + + /// + /// Event raised when the WebSocket connection is re-established after a disconnect. + /// + public Event Reconnecting => _client.Reconnecting; + /// /// Event handler for AssistantEnd. /// Use AssistantEnd.Subscribe(...) to receive messages. /// - public readonly Event AssistantEnd = new(); + public Event AssistantEnd { get; } = new(); /// /// Event handler for AssistantMessage. /// Use AssistantMessage.Subscribe(...) to receive messages. /// - public readonly Event AssistantMessage = new(); + public Event AssistantMessage { get; } = new(); /// /// Event handler for AssistantProsody. /// Use AssistantProsody.Subscribe(...) to receive messages. /// - public readonly Event AssistantProsody = new(); + public Event AssistantProsody { get; } = new(); /// /// Event handler for AudioOutput. /// Use AudioOutput.Subscribe(...) to receive messages. /// - public readonly Event AudioOutput = new(); + public Event AudioOutput { get; } = new(); /// /// Event handler for ChatMetadata. /// Use ChatMetadata.Subscribe(...) to receive messages. /// - public readonly Event ChatMetadata = new(); + public Event ChatMetadata { get; } = new(); /// /// Event handler for WebSocketError. /// Use WebSocketError.Subscribe(...) to receive messages. /// - public readonly Event WebSocketError = new(); + public Event WebSocketError { get; } = new(); /// /// Event handler for UserInterruption. /// Use UserInterruption.Subscribe(...) to receive messages. /// - public readonly Event UserInterruption = new(); + public Event UserInterruption { get; } = new(); /// /// Event handler for UserMessage. /// Use UserMessage.Subscribe(...) to receive messages. /// - public readonly Event UserMessage = new(); + public Event UserMessage { get; } = new(); /// /// Event handler for ToolCallMessage. /// Use ToolCallMessage.Subscribe(...) to receive messages. /// - public readonly Event ToolCallMessage = new(); + public Event ToolCallMessage { get; } = new(); /// /// Event handler for ToolResponseMessage. /// Use ToolResponseMessage.Subscribe(...) to receive messages. /// - public readonly Event ToolResponseMessage = new(); + public Event ToolResponseMessage { get; } = new(); /// /// Event handler for ToolErrorMessage. /// Use ToolErrorMessage.Subscribe(...) to receive messages. /// - public readonly Event ToolErrorMessage = new(); + public Event ToolErrorMessage { get; } = new(); /// /// Event handler for SessionSettings. /// Use SessionSettings.Subscribe(...) to receive messages. /// - public readonly Event SessionSettings = new(); + public Event SessionSettings { get; } = new(); /// /// Event handler for unknown/unrecognized message types. /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. /// - public readonly Event UnknownMessage = new(); - - /// - /// Constructor with options - /// - public ControlPlaneApi(ControlPlaneApi.Options options) - { - _options = options; - var uri = new UriBuilder(_options.BaseUrl) - { - Query = new Hume.Core.QueryStringBuilder.Builder(capacity: 1) - .Add("access_token", _options.AccessToken) - .Build(), - }; - uri.Path = $"{uri.Path.TrimEnd('/')}/chat/{Uri.EscapeDataString(_options.ChatId)}/connect"; - _client = new WebSocketClient(uri.Uri, OnTextMessage); - _client.HttpInvoker = _options.HttpInvoker; - _client.IsReconnectionEnabled = _options.IsReconnectionEnabled; - _client.ReconnectTimeout = _options.ReconnectTimeout; - _client.ErrorReconnectTimeout = _options.ErrorReconnectTimeout; - _client.LostReconnectTimeout = _options.LostReconnectTimeout; - _client.Backoff = _options.ReconnectBackoff; - } - - /// - /// Gets the current connection status of the WebSocket. - /// - public ConnectionStatus Status => _client.Status; - - /// - /// Event that is raised when the WebSocket connection is established. - /// - public Event Connected => _client.Connected; - - /// - /// Event that is raised when the WebSocket connection is closed. - /// - public Event Closed => _client.Closed; - - /// - /// Event that is raised when an exception occurs during WebSocket operations. - /// - public Event ExceptionOccurred => _client.ExceptionOccurred; - - /// - /// Event raised when the WebSocket connection is re-established after a disconnect. - /// - public Event Reconnecting => _client.Reconnecting; + public Event UnknownMessage { get; } = new(); /// /// Disposes of event subscriptions diff --git a/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneApi.cs b/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneApi.cs index 9daf4df..bedeeac 100644 --- a/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneApi.cs +++ b/src/Hume/EmpathicVoice/ControlPlane/IControlPlaneApi.cs @@ -1,3 +1,4 @@ +using global::System.Text.Json; using Hume.Core.WebSockets; using OneOf; @@ -9,6 +10,19 @@ public partial interface IControlPlaneApi : IAsyncDisposable, IDisposable public Event Closed { get; } public Event ExceptionOccurred { get; } public Event Reconnecting { get; } + public Event AssistantEnd { get; } + public Event AssistantMessage { get; } + public Event AssistantProsody { get; } + public Event AudioOutput { get; } + public Event ChatMetadata { get; } + public Event WebSocketError { get; } + public Event UserInterruption { get; } + public Event UserMessage { get; } + public Event ToolCallMessage { get; } + public Event ToolResponseMessage { get; } + public Event ToolErrorMessage { get; } + public Event SessionSettings { get; } + public Event UnknownMessage { get; } public ConnectionStatus Status { get; } global::System.Threading.Tasks.Task ConnectAsync(CancellationToken cancellationToken = default); diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/IStreamApi.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/IStreamApi.cs index 1658240..88019b4 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/IStreamApi.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/IStreamApi.cs @@ -1,3 +1,4 @@ +using global::System.Text.Json; using Hume.Core.WebSockets; namespace Hume.ExpressionMeasurement.Stream; @@ -8,6 +9,10 @@ public partial interface IStreamApi : IAsyncDisposable, IDisposable public Event Closed { get; } public Event ExceptionOccurred { get; } public Event Reconnecting { get; } + public Event StreamModelPredictions { get; } + public Event StreamErrorMessage { get; } + public Event StreamWarningMessage { get; } + public Event UnknownMessage { get; } public ConnectionStatus Status { get; } global::System.Threading.Tasks.Task ConnectAsync(CancellationToken cancellationToken = default); diff --git a/src/Hume/ExpressionMeasurement/Stream/Stream/StreamApi.cs b/src/Hume/ExpressionMeasurement/Stream/Stream/StreamApi.cs index fb19865..23f5fe5 100644 --- a/src/Hume/ExpressionMeasurement/Stream/Stream/StreamApi.cs +++ b/src/Hume/ExpressionMeasurement/Stream/Stream/StreamApi.cs @@ -20,30 +20,6 @@ public event PropertyChangedEventHandler PropertyChanged remove => _client.PropertyChanged -= value; } - /// - /// Event handler for StreamModelPredictions. - /// Use StreamModelPredictions.Subscribe(...) to receive messages. - /// - public readonly Event StreamModelPredictions = new(); - - /// - /// Event handler for StreamErrorMessage. - /// Use StreamErrorMessage.Subscribe(...) to receive messages. - /// - public readonly Event StreamErrorMessage = new(); - - /// - /// Event handler for StreamWarningMessage. - /// Use StreamWarningMessage.Subscribe(...) to receive messages. - /// - public readonly Event StreamWarningMessage = new(); - - /// - /// Event handler for unknown/unrecognized message types. - /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. - /// - public readonly Event UnknownMessage = new(); - /// /// Default constructor /// @@ -91,6 +67,30 @@ public StreamApi(StreamApi.Options options) /// public Event Reconnecting => _client.Reconnecting; + /// + /// Event handler for StreamModelPredictions. + /// Use StreamModelPredictions.Subscribe(...) to receive messages. + /// + public Event StreamModelPredictions { get; } = new(); + + /// + /// Event handler for StreamErrorMessage. + /// Use StreamErrorMessage.Subscribe(...) to receive messages. + /// + public Event StreamErrorMessage { get; } = new(); + + /// + /// Event handler for StreamWarningMessage. + /// Use StreamWarningMessage.Subscribe(...) to receive messages. + /// + public Event StreamWarningMessage { get; } = new(); + + /// + /// Event handler for unknown/unrecognized message types. + /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. + /// + public Event UnknownMessage { get; } = new(); + /// /// Disposes of event subscriptions /// diff --git a/src/Hume/Tts/StreamInput/IStreamInputApi.cs b/src/Hume/Tts/StreamInput/IStreamInputApi.cs index 1c61585..76d5d9d 100644 --- a/src/Hume/Tts/StreamInput/IStreamInputApi.cs +++ b/src/Hume/Tts/StreamInput/IStreamInputApi.cs @@ -1,3 +1,4 @@ +using global::System.Text.Json; using Hume.Core.WebSockets; namespace Hume.Tts; @@ -8,6 +9,9 @@ public partial interface IStreamInputApi : IAsyncDisposable, IDisposable public Event Closed { get; } public Event ExceptionOccurred { get; } public Event Reconnecting { get; } + public Event SnippetAudioChunk { get; } + public Event TimestampMessage { get; } + public Event UnknownMessage { get; } public ConnectionStatus Status { get; } global::System.Threading.Tasks.Task ConnectAsync(CancellationToken cancellationToken = default); diff --git a/src/Hume/Tts/StreamInput/StreamInputApi.cs b/src/Hume/Tts/StreamInput/StreamInputApi.cs index 069b821..a6fa96d 100644 --- a/src/Hume/Tts/StreamInput/StreamInputApi.cs +++ b/src/Hume/Tts/StreamInput/StreamInputApi.cs @@ -27,24 +27,6 @@ public event PropertyChangedEventHandler PropertyChanged remove => _client.PropertyChanged -= value; } - /// - /// Event handler for SnippetAudioChunk. - /// Use SnippetAudioChunk.Subscribe(...) to receive messages. - /// - public readonly Event SnippetAudioChunk = new(); - - /// - /// Event handler for TimestampMessage. - /// Use TimestampMessage.Subscribe(...) to receive messages. - /// - public readonly Event TimestampMessage = new(); - - /// - /// Event handler for unknown/unrecognized message types. - /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. - /// - public readonly Event UnknownMessage = new(); - /// /// Default constructor /// @@ -105,6 +87,24 @@ public StreamInputApi(StreamInputApi.Options options) /// public Event Reconnecting => _client.Reconnecting; + /// + /// Event handler for SnippetAudioChunk. + /// Use SnippetAudioChunk.Subscribe(...) to receive messages. + /// + public Event SnippetAudioChunk { get; } = new(); + + /// + /// Event handler for TimestampMessage. + /// Use TimestampMessage.Subscribe(...) to receive messages. + /// + public Event TimestampMessage { get; } = new(); + + /// + /// Event handler for unknown/unrecognized message types. + /// Use UnknownMessage.Subscribe(...) to handle messages from newer server versions. + /// + public Event UnknownMessage { get; } = new(); + /// /// Disposes of event subscriptions /// From 0beb4d5d1f693885880c1ffaa600b922a64f8d96 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 10 Apr 2026 12:47:28 -0400 Subject: [PATCH 3/5] fix: add domain events to websocket interfaces and bump CI to .NET 10 The generator introduced IChatApi/IControlPlaneApi/IStreamApi/IStreamInputApi interfaces but omitted domain-specific event properties (e.g. AssistantMessage, ChatMetadata, AudioOutput). This broke consumers using the interface return type from CreateChatApi(). Also bumps CI dotnet-version to 10.x to support net9.0 targets. --- .github/workflows/ci.yml | 8 ++++---- examples-repo | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) create mode 160000 examples-repo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70846d8..52b4234 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.x + dotnet-version: 10.x - name: Install tools run: | @@ -39,7 +39,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.x + dotnet-version: 10.x - name: Install tools run: | @@ -61,7 +61,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.x + dotnet-version: 10.x - name: Publish env: @@ -93,7 +93,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 9.x + dotnet-version: 10.x - name: Build SDK working-directory: ./sdk diff --git a/examples-repo b/examples-repo new file mode 160000 index 0000000..8e59714 --- /dev/null +++ b/examples-repo @@ -0,0 +1 @@ +Subproject commit 8e597143196697b94fb3940c31007da2d334136c From 1b39561f921692a942d83a96a330c6664663a3c2 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:39:11 -0400 Subject: [PATCH 4/5] Deduplicate CI wf runs --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52b4234..2736db7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,10 @@ name: ci on: push: + branches: [main] + tags: ['*'] pull_request: - types: [synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened, ready_for_review] jobs: compile: From 00929c90999a095d69a833ef75f98c778fad2fa3 Mon Sep 17 00:00:00 2001 From: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:48:12 -0400 Subject: [PATCH 5/5] Fix ci.yml --- .github/workflows/ci.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2736db7..21beb88 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 - - - uses: actions/checkout@master + uses: actions/checkout@v6 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x @@ -34,12 +32,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 - - - uses: actions/checkout@master + uses: actions/checkout@v6 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x @@ -58,10 +54,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x @@ -79,7 +75,7 @@ jobs: contents: read steps: - name: Checkout SDK repo (PR branch) - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: sdk # checkout the PR branch, not main @@ -87,13 +83,13 @@ jobs: fetch-depth: 0 - name: Checkout examples repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: humeai/hume-api-examples path: examples - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x