From b5bf149791e6b7968183ebe69628a97134e61907 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 24 Sep 2026 11:34:22 +0200 Subject: [PATCH 01/50] DEVEXP-1618: Add Voice v2 calls.start (createCall) endpoint --- CHANGELOG.md | 10 + client/resources/config-default.properties | 2 + .../src/main/com/sinch/sdk/SinchClient.java | 17 +- .../sinch/sdk/domains/voice/VoiceService.java | 9 + .../domains/voice/adapters/VoiceService.java | 29 ++ .../domains/voice/api/v2/VoiceService.java | 19 + .../voice/api/v2/adapters/VoiceService.java | 96 +++++ .../domains/voice/api/v2/package-info.java | 6 + .../models/v2/calls/request/package-info.java | 6 + .../v2/calls/response/package-info.java | 6 + .../v2/destination/CallDestination.java | 13 + .../models/v2/destination/CallOrigin.java | 12 + .../models/v2/destination/package-info.java | 6 + .../domains/voice/models/v2/package-info.java | 6 + .../voice/models/v2/svaml/Message.java | 12 + .../voice/models/v2/svaml/SvamlCommand.java | 12 + .../voice/models/v2/svaml/package-info.java | 6 + .../com/sinch/sdk/models/VoiceContext.java | 46 ++- .../CallsServiceIdempotencyKeyTest.java | 154 ++++++++ .../api/v2/adapters/CallsServiceTest.java | 162 ++++++++ .../api/v2/adapters/VoiceServiceTest.java | 90 +++++ .../test/java/com/sinch/sdk/e2e/Config.java | 2 + .../sdk/e2e/domains/voice/v2/CallsSteps.java | 99 +++++ .../sdk/e2e/domains/voice/v2/VoiceV2IT.java | 24 ++ .../snippets/src/main/java/voice/README.md | 3 + .../src/main/java/voice/v2/calls/Start.java | 94 +++++ .../domains/voice/api/v2/CallsService.java | 23 ++ .../api/v2/adapters/CallsServiceImpl.java | 131 +++++++ .../v2/calls/request/StartCallRequest.java | 101 +++++ .../calls/request/StartCallRequestImpl.java | 139 +++++++ .../v2/calls/response/StartCallResponse.java | 86 +++++ .../calls/response/StartCallResponseImpl.java | 147 +++++++ .../models/v2/destination/CallHeader.java | 62 +++ .../models/v2/destination/CallHeaderImpl.java | 114 ++++++ .../voice/models/v2/destination/Phone.java | 74 ++++ .../models/v2/destination/PhoneDetails.java | 45 +++ .../v2/destination/PhoneDetailsImpl.java | 90 +++++ .../models/v2/destination/PhoneImpl.java | 117 ++++++ .../voice/models/v2/destination/Sip.java | 73 ++++ .../models/v2/destination/SipCallHeader.java | 62 +++ .../v2/destination/SipCallHeaderImpl.java | 115 ++++++ .../models/v2/destination/SipDetails.java | 116 ++++++ .../models/v2/destination/SipDetailsImpl.java | 148 +++++++ .../voice/models/v2/destination/SipFrom.java | 73 ++++ .../models/v2/destination/SipFromDetails.java | 64 +++ .../v2/destination/SipFromDetailsImpl.java | 118 ++++++ .../models/v2/destination/SipFromImpl.java | 117 ++++++ .../voice/models/v2/destination/SipImpl.java | 117 ++++++ .../voice/models/v2/destination/Stream.java | 74 ++++ .../models/v2/destination/StreamDetails.java | 81 ++++ .../v2/destination/StreamDetailsImpl.java | 148 +++++++ .../models/v2/destination/StreamImpl.java | 117 ++++++ .../models/v2/destination/StreamOptions.java | 148 +++++++ .../v2/destination/StreamOptionsImpl.java | 147 +++++++ .../models/v2/destination/VoiceRelay.java | 77 ++++ .../v2/destination/VoiceRelayDetails.java | 129 +++++++ .../v2/destination/VoiceRelayDetailsImpl.java | 205 ++++++++++ .../models/v2/destination/VoiceRelayImpl.java | 119 ++++++ .../internal/CallDestinationInternal.java | 6 + .../internal/CallDestinationInternalImpl.java | 182 +++++++++ .../internal/CallOriginInternal.java | 6 + .../internal/CallOriginInternalImpl.java | 170 ++++++++ .../voice/models/v2/svaml/AmdCommand.java | 81 ++++ .../voice/models/v2/svaml/AmdCommandImpl.java | 118 ++++++ .../voice/models/v2/svaml/AmdEvents.java | 97 +++++ .../voice/models/v2/svaml/AmdEventsImpl.java | 175 +++++++++ .../voice/models/v2/svaml/AnswerCommand.java | 60 +++ .../models/v2/svaml/AnswerCommandImpl.java | 93 +++++ .../models/v2/svaml/BridgeCallCommand.java | 83 ++++ .../v2/svaml/BridgeCallCommandImpl.java | 122 ++++++ .../voice/models/v2/svaml/CallEvents.java | 125 ++++++ .../voice/models/v2/svaml/CallEventsImpl.java | 229 +++++++++++ .../models/v2/svaml/CustomEventCommand.java | 121 ++++++ .../v2/svaml/CustomEventCommandImpl.java | 177 +++++++++ .../voice/models/v2/svaml/DialCommand.java | 175 +++++++++ .../models/v2/svaml/DialCommandImpl.java | 267 +++++++++++++ .../models/v2/svaml/GotoMenuCommand.java | 81 ++++ .../models/v2/svaml/GotoMenuCommandImpl.java | 122 ++++++ .../voice/models/v2/svaml/HangupCommand.java | 84 ++++ .../models/v2/svaml/HangupCommandImpl.java | 121 ++++++ .../voice/models/v2/svaml/MenuCommand.java | 99 +++++ .../models/v2/svaml/MenuCommandImpl.java | 151 ++++++++ .../voice/models/v2/svaml/MenuItem.java | 249 ++++++++++++ .../voice/models/v2/svaml/MenuItemImpl.java | 363 ++++++++++++++++++ .../voice/models/v2/svaml/MenuPrompt.java | 69 ++++ .../voice/models/v2/svaml/MenuPromptImpl.java | 120 ++++++ .../voice/models/v2/svaml/MessageEvents.java | 45 +++ .../models/v2/svaml/MessageEventsImpl.java | 91 +++++ .../models/v2/svaml/MessagesCommand.java | 119 ++++++ .../models/v2/svaml/MessagesCommandImpl.java | 178 +++++++++ .../voice/models/v2/svaml/PauseCommand.java | 81 ++++ .../models/v2/svaml/PauseCommandImpl.java | 124 ++++++ .../domains/voice/models/v2/svaml/Play.java | 45 +++ .../voice/models/v2/svaml/PlayImpl.java | 90 +++++ .../voice/models/v2/svaml/PlayMessage.java | 78 ++++ .../models/v2/svaml/PlayMessageImpl.java | 118 ++++++ .../v2/svaml/RecordingDestinationType.java | 46 +++ .../models/v2/svaml/RecordingEvents.java | 63 +++ .../models/v2/svaml/RecordingEventsImpl.java | 120 ++++++ .../models/v2/svaml/RecordingFormatType.java | 35 ++ .../models/v2/svaml/RecordingOptions.java | 130 +++++++ .../models/v2/svaml/RecordingOptionsImpl.java | 232 +++++++++++ .../voice/models/v2/svaml/RecordingType.java | 38 ++ .../domains/voice/models/v2/svaml/Say.java | 116 ++++++ .../voice/models/v2/svaml/SayImpl.java | 147 +++++++ .../voice/models/v2/svaml/SayMessage.java | 78 ++++ .../voice/models/v2/svaml/SayMessageImpl.java | 117 ++++++ .../v2/svaml/StartRecordingCommand.java | 119 ++++++ .../v2/svaml/StartRecordingCommandImpl.java | 178 +++++++++ .../models/v2/svaml/StopMessagesCommand.java | 82 ++++ .../v2/svaml/StopMessagesCommandImpl.java | 122 ++++++ .../models/v2/svaml/StopRecordingCommand.java | 81 ++++ .../v2/svaml/StopRecordingCommandImpl.java | 122 ++++++ .../models/v2/svaml/TranscriptionOptions.java | 62 +++ .../v2/svaml/TranscriptionOptionsImpl.java | 119 ++++++ .../v2/svaml/internal/MessageInternal.java | 6 + .../svaml/internal/MessageInternalImpl.java | 170 ++++++++ .../svaml/internal/SvamlCommandInternal.java | 6 + .../internal/SvamlCommandInternalImpl.java | 214 +++++++++++ .../request/StartCallRequestDtoTest.java | 32 ++ .../response/StartCallResponseDtoTest.java | 39 ++ .../v2/destination/CallHeaderDtoTest.java | 34 ++ .../v2/destination/PhoneDetailsDtoTest.java | 34 ++ .../models/v2/destination/PhoneDtoTest.java | 34 ++ .../v2/destination/SipCallHeaderDtoTest.java | 34 ++ .../v2/destination/SipDetailsDtoTest.java | 39 ++ .../models/v2/destination/SipDtoTest.java | 34 ++ .../v2/destination/SipFromDetailsDtoTest.java | 37 ++ .../models/v2/destination/SipFromDtoTest.java | 34 ++ .../v2/destination/StreamDetailsDtoTest.java | 39 ++ .../models/v2/destination/StreamDtoTest.java | 34 ++ .../v2/destination/StreamOptionsDtoTest.java | 38 ++ .../destination/VoiceRelayDetailsDtoTest.java | 42 ++ .../v2/destination/VoiceRelayDtoTest.java | 36 ++ .../models/v2/svaml/AmdCommandDtoTest.java | 34 ++ .../models/v2/svaml/AmdEventsDtoTest.java | 40 ++ .../models/v2/svaml/AnswerCommandDtoTest.java | 33 ++ .../v2/svaml/BridgeCallCommandDtoTest.java | 35 ++ .../models/v2/svaml/CallEventsDtoTest.java | 53 +++ .../v2/svaml/CustomEventCommandDtoTest.java | 39 ++ .../models/v2/svaml/DialCommandDtoTest.java | 110 ++++++ .../v2/svaml/GotoMenuCommandDtoTest.java | 35 ++ .../models/v2/svaml/HangupCommandDtoTest.java | 34 ++ .../models/v2/svaml/MenuCommandDtoTest.java | 38 ++ .../models/v2/svaml/MenuItemDtoTest.java | 49 +++ .../models/v2/svaml/MenuPromptDtoTest.java | 38 ++ .../models/v2/svaml/MessageEventsDtoTest.java | 37 ++ .../v2/svaml/MessagesCommandDtoTest.java | 42 ++ .../models/v2/svaml/PauseCommandDtoTest.java | 34 ++ .../voice/models/v2/svaml/PlayDtoTest.java | 34 ++ .../models/v2/svaml/PlayMessageDtoTest.java | 34 ++ .../v2/svaml/RecordingEventsDtoTest.java | 39 ++ .../v2/svaml/RecordingOptionsDtoTest.java | 42 ++ .../voice/models/v2/svaml/SayDtoTest.java | 38 ++ .../models/v2/svaml/SayMessageDtoTest.java | 34 ++ .../svaml/StartRecordingCommandDtoTest.java | 39 ++ .../v2/svaml/StopMessagesCommandDtoTest.java | 35 ++ .../v2/svaml/StopRecordingCommandDtoTest.java | 35 ++ .../models/v2/svaml/SvamlCommandTest.java | 29 ++ .../v2/svaml/TranscriptionOptionsDtoTest.java | 35 ++ .../v2/calls/request/StartCallRequestDto.json | 215 +++++++++++ .../calls/response/StartCallResponseDto.json | 5 + .../voice/v2/destination/CallHeaderDto.json | 4 + .../voice/v2/destination/PhoneDetailsDto.json | 3 + .../voice/v2/destination/PhoneDto.json | 6 + .../v2/destination/SipCallHeaderDto.json | 4 + .../voice/v2/destination/SipDetailsDto.json | 10 + .../domains/voice/v2/destination/SipDto.json | 13 + .../v2/destination/SipFromDetailsDto.json | 4 + .../voice/v2/destination/SipFromDto.json | 7 + .../v2/destination/StreamDetailsDto.json | 14 + .../voice/v2/destination/StreamDto.json | 17 + .../v2/destination/StreamOptionsDto.json | 5 + .../v2/destination/VoiceRelayDetailsDto.json | 12 + .../voice/v2/destination/VoiceRelayDto.json | 15 + .../domains/voice/v2/svaml/AmdCommandDto.json | 53 +++ .../domains/voice/v2/svaml/AmdEventsDto.json | 50 +++ .../voice/v2/svaml/AnswerCommandDto.json | 3 + .../voice/v2/svaml/BridgeCallCommandDto.json | 4 + .../domains/voice/v2/svaml/CallEventsDto.json | 193 ++++++++++ .../voice/v2/svaml/CustomEventCommandDto.json | 6 + .../voice/v2/svaml/DialCommandDto.json | 211 ++++++++++ .../voice/v2/svaml/DialCommandSipDto.json | 24 ++ .../voice/v2/svaml/DialCommandStreamDto.json | 20 + .../v2/svaml/DialCommandVoiceRelayDto.json | 18 + .../voice/v2/svaml/GotoMenuCommandDto.json | 4 + .../voice/v2/svaml/HangupCommandDto.json | 4 + .../voice/v2/svaml/MenuCommandDto.json | 56 +++ .../domains/voice/v2/svaml/MenuItemDto.json | 50 +++ .../domains/voice/v2/svaml/MenuPromptDto.json | 13 + .../voice/v2/svaml/MessageEventsDto.json | 8 + .../voice/v2/svaml/MessagesCommandDto.json | 28 ++ .../voice/v2/svaml/PauseCommandDto.json | 4 + .../domains/voice/v2/svaml/PlayDto.json | 3 + .../voice/v2/svaml/PlayMessageDto.json | 6 + .../voice/v2/svaml/RecordingEventsDto.json | 14 + .../voice/v2/svaml/RecordingOptionsDto.json | 11 + .../domains/voice/v2/svaml/SayDto.json | 5 + .../domains/voice/v2/svaml/SayMessageDto.json | 8 + .../v2/svaml/StartRecordingCommandDto.json | 29 ++ .../v2/svaml/StopMessagesCommandDto.json | 4 + .../v2/svaml/StopRecordingCommandDto.json | 4 + .../v2/svaml/TranscriptionOptionsDto.json | 4 + pom.xml | 1 + 204 files changed, 14191 insertions(+), 2 deletions(-) create mode 100644 client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/api/v2/package-info.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/package-info.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/package-info.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallDestination.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallOrigin.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/package-info.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/package-info.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Message.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlCommand.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/package-info.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceIdempotencyKeyTest.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java create mode 100644 client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java create mode 100644 client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceV2IT.java create mode 100644 examples/snippets/src/main/java/voice/v2/calls/Start.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponseImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallHeader.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallHeaderImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Phone.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetails.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetailsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Sip.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeader.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeaderImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipDetails.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipDetailsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFrom.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetails.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetailsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Stream.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetails.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetailsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptions.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptionsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelay.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetails.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetailsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallDestinationInternal.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallDestinationInternalImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallOriginInternal.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallOriginInternalImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEvents.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEventsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CallEvents.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CallEventsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItem.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItemImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPrompt.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPromptImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEvents.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEventsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Play.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessage.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessageImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingDestinationType.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEvents.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEventsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingFormatType.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptions.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptionsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingType.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Say.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessage.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessageImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommand.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommandImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptions.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptionsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/MessageInternal.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/MessageInternalImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/SvamlCommandInternal.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/SvamlCommandInternalImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponseDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/CallHeaderDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetailsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeaderDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipDetailsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetailsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetailsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptionsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetailsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEventsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/CallEventsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItemDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPromptDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEventsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PlayDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessageDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEventsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptionsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SayDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessageDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommandDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlCommandTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptionsDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/calls/request/StartCallRequestDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/calls/response/StartCallResponseDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/CallHeaderDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/PhoneDetailsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/PhoneDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/SipCallHeaderDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/SipDetailsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/SipDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/SipFromDetailsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/SipFromDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamDetailsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamOptionsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/VoiceRelayDetailsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/destination/VoiceRelayDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/AmdCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/AmdEventsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/AnswerCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/BridgeCallCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/CallEventsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/CustomEventCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandSipDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandStreamDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandVoiceRelayDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/GotoMenuCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/HangupCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuItemDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuPromptDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/MessageEventsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/MessagesCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/PauseCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/PlayDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/PlayMessageDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/RecordingEventsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/RecordingOptionsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/SayDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/SayMessageDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/StartRecordingCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/StopMessagesCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/StopRecordingCommandDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/TranscriptionOptionsDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a735ee66e..6c2309788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ All notable changes to the **Sinch Java SDK** are documented in this file. > - `[tech]` — technical improvement --- +## v2.3.0 - Unreleased + +### Voice +- **[feature]** Support [Voice API v2](https://developers.sinch.com/docs/voice-2.0), exposed under `voice().v2()` +- **[feature]** Voice V2 Calls API: `start` operation to create an outbound call + +### Examples / Snippets +- **[doc]** New `voice/v2/calls/Start` snippet + + ## v2.2.0 - 2026-09-17 ### SDK diff --git a/client/resources/config-default.properties b/client/resources/config-default.properties index 102be6245..631634ed5 100644 --- a/client/resources/config-default.properties +++ b/client/resources/config-default.properties @@ -18,6 +18,8 @@ voice-server-south-america=https://calling-sae1.api.sinch.com voice-server-south-east-asia1=https://calling-apse1.api.sinch.com voice-server-south-east-asia2=https://calling-apse2.api.sinch.com +voice-v2-server=https://voice.api.sinch.com + conversation-region= conversation-server=https://%s.conversation.api.sinch.com template-management-conversation-server=https://%s.template.api.sinch.com diff --git a/client/src/main/com/sinch/sdk/SinchClient.java b/client/src/main/com/sinch/sdk/SinchClient.java index a4e392f5c..c02e2cedf 100644 --- a/client/src/main/com/sinch/sdk/SinchClient.java +++ b/client/src/main/com/sinch/sdk/SinchClient.java @@ -46,6 +46,7 @@ public class SinchClient { private static final String VOICE_REGION_KEY = "voice-region"; private static final String VOICE_APPLICATION_MANAGEMENT_SERVER_KEY = "voice-application-management-server"; + private static final String VOICE_V2_SERVER_KEY = "voice-v2-server"; private static final String VERIFICATION_SERVER_KEY = "verification-server"; @@ -182,6 +183,9 @@ private void handleDefaultVoiceSettings( .map(VoiceContext::getVoiceApplicationManagementUrl) .orElse(null); + String voiceV2Url = + configuration.getVoiceContext().map(VoiceContext::getVoiceV2Url).orElse(null); + // default region to be used ? if (null == region && props.containsKey(VOICE_REGION_KEY)) { region = VoiceRegion.from(props.getProperty(VOICE_REGION_KEY)); @@ -200,12 +204,21 @@ private void handleDefaultVoiceSettings( voiceApplicationManagementUrl = props.getProperty(VOICE_APPLICATION_MANAGEMENT_SERVER_KEY); } - if (null != region || null != voiceUrl || null != voiceApplicationManagementUrl) { + // Voice V2 server + if (StringUtil.isEmpty(voiceV2Url) && props.containsKey(VOICE_V2_SERVER_KEY)) { + voiceV2Url = props.getProperty(VOICE_V2_SERVER_KEY); + } + + if (null != region + || null != voiceUrl + || null != voiceApplicationManagementUrl + || null != voiceV2Url) { builder.setVoiceContext( VoiceContext.builder() .setVoiceRegion(region) .setVoiceUrl(voiceUrl) .setVoiceApplicationMngmtUrl(voiceApplicationManagementUrl) + .setVoiceV2Url(voiceV2Url) .build()); } } @@ -414,7 +427,9 @@ private VerificationService verificationInit() { private VoiceService voiceInit() { return new com.sinch.sdk.domains.voice.adapters.VoiceService( getConfiguration().getApplicationCredentials().orElse(null), + getConfiguration().getUnifiedCredentials().orElse(null), getConfiguration().getVoiceContext().orElse(null), + getConfiguration().getOAuthServer(), this::getHttpClient); } diff --git a/client/src/main/com/sinch/sdk/domains/voice/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/VoiceService.java index 3f386380f..8811fdc04 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/VoiceService.java @@ -17,4 +17,13 @@ public interface VoiceService { * @since 1.1 */ com.sinch.sdk.domains.voice.api.v1.VoiceService v1(); + + /** + * Voice Service V2 + * + * @return V2 service instance for project + * @see Documentation + * @since 2.3 + */ + com.sinch.sdk.domains.voice.api.v2.VoiceService v2(); } diff --git a/client/src/main/com/sinch/sdk/domains/voice/adapters/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/adapters/VoiceService.java index 8037bf697..a72aba03f 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/adapters/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/adapters/VoiceService.java @@ -1,24 +1,40 @@ package com.sinch.sdk.domains.voice.adapters; import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.models.ApplicationCredentials; +import com.sinch.sdk.models.UnifiedCredentials; import com.sinch.sdk.models.VoiceContext; import java.util.function.Supplier; public class VoiceService implements com.sinch.sdk.domains.voice.VoiceService { private final ApplicationCredentials credentials; + private final UnifiedCredentials unifiedCredentials; private final VoiceContext context; + private final ServerConfiguration oAuthServer; private final Supplier httpClientSupplier; private volatile com.sinch.sdk.domains.voice.api.v1.VoiceService v1; + private volatile com.sinch.sdk.domains.voice.api.v2.VoiceService v2; public VoiceService( ApplicationCredentials credentials, VoiceContext context, Supplier httpClientSupplier) { + this(credentials, null, context, null, httpClientSupplier); + } + + public VoiceService( + ApplicationCredentials credentials, + UnifiedCredentials unifiedCredentials, + VoiceContext context, + ServerConfiguration oAuthServer, + Supplier httpClientSupplier) { this.credentials = credentials; + this.unifiedCredentials = unifiedCredentials; this.context = context; + this.oAuthServer = oAuthServer; this.httpClientSupplier = httpClientSupplier; } @@ -34,4 +50,17 @@ public com.sinch.sdk.domains.voice.api.v1.VoiceService v1() { } return this.v1; } + + public com.sinch.sdk.domains.voice.api.v2.VoiceService v2() { + if (null == this.v2) { + synchronized (this) { + if (null == this.v2) { + this.v2 = + new com.sinch.sdk.domains.voice.api.v2.adapters.VoiceService( + unifiedCredentials, context, oAuthServer, httpClientSupplier); + } + } + } + return this.v2; + } } diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java new file mode 100644 index 000000000..3a0cd07fa --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java @@ -0,0 +1,19 @@ +package com.sinch.sdk.domains.voice.api.v2; + +/** + * Voice Service V2 + * + * @see https://developers.sinch.com/docs/voice-2.0 + * @since 2.3 + */ +public interface VoiceService { + + /** + * Calls Service instance + * + * @return service instance for project + * @since 2.3 + */ + CallsService calls(); +} diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java new file mode 100644 index 000000000..3be07b816 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java @@ -0,0 +1,96 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import com.sinch.sdk.auth.adapters.OAuthManager; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.core.utils.StringUtil; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.models.UnifiedCredentials; +import com.sinch.sdk.models.VoiceContext; +import java.util.AbstractMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class VoiceService implements com.sinch.sdk.domains.voice.api.v2.VoiceService { + + private static final Logger LOGGER = Logger.getLogger(VoiceService.class.getName()); + private static final String SECURITY_SCHEME_KEYWORD_VOICE = "SinchOAuth2"; + + private final UnifiedCredentials credentials; + private final VoiceContext context; + private final ServerConfiguration oAuthServer; + private final Supplier httpClientSupplier; + + private volatile String uriUUID; + private volatile Map authManagers; + + private volatile CallsService calls; + + public VoiceService( + UnifiedCredentials credentials, + VoiceContext context, + ServerConfiguration oAuthServer, + Supplier httpClientSupplier) { + this.credentials = credentials; + this.context = context; + this.oAuthServer = oAuthServer; + this.httpClientSupplier = httpClientSupplier; + } + + public CallsService calls() { + if (null == this.calls) { + synchronized (this) { + if (null == this.calls) { + instanceLazyInit(); + this.calls = + new CallsServiceImpl( + httpClientSupplier.get(), + context.getVoiceV2Server(), + authManagers, + HttpMapper.getInstance(), + uriUUID); + } + } + } + return this.calls; + } + + private void instanceLazyInit() { + if (null != this.authManagers) { + return; + } + synchronized (this) { + if (null == this.authManagers) { + Objects.requireNonNull( + credentials, "Voice V2 service requires unified credentials to be defined"); + Objects.requireNonNull(context, "Voice V2 service requires context to be defined"); + StringUtil.requireNonEmpty( + credentials.getKeyId(), "Voice V2 service requires 'keyId' to be defined"); + StringUtil.requireNonEmpty( + credentials.getKeySecret(), "Voice V2 service requires 'keySecret' to be defined"); + StringUtil.requireNonEmpty( + credentials.getProjectId(), "Voice V2 service requires 'projectId' to be defined"); + StringUtil.requireNonEmpty( + context.getVoiceV2Url(), "Voice V2 service requires 'voiceV2Url' to be defined"); + + LOGGER.fine( + "Activate Voice V2 API with server='" + context.getVoiceV2Server().getUrl() + "'"); + + AuthManager authManager = + new OAuthManager( + credentials, oAuthServer, HttpMapper.getInstance(), httpClientSupplier); + + uriUUID = credentials.getProjectId(); + authManagers = + Stream.of(new AbstractMap.SimpleEntry<>(SECURITY_SCHEME_KEYWORD_VOICE, authManager)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + } + } +} diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/package-info.java new file mode 100644 index 000000000..bb56059c0 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/package-info.java @@ -0,0 +1,6 @@ +/** + * Voice API interface V2 + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.api.v2; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/package-info.java new file mode 100644 index 000000000..6a0febb56 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 calls requests + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.calls.request; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/package-info.java new file mode 100644 index 000000000..b47de6553 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 calls responses + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.calls.response; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallDestination.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallDestination.java new file mode 100644 index 000000000..bfb2126c9 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallDestination.java @@ -0,0 +1,13 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.destination.internal.CallDestinationInternalImpl; + +/** + * Base interface for the destination of a call (the to of a dial + * command). + * + * @since 2.3 + */ +@JsonDeserialize(using = CallDestinationInternalImpl.Deserializer.class) +public interface CallDestination {} diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallOrigin.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallOrigin.java new file mode 100644 index 000000000..e9fb4cc3f --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallOrigin.java @@ -0,0 +1,12 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.destination.internal.CallOriginInternalImpl; + +/** + * Base interface for the origin of a call (the from of a dial command). + * + * @since 2.3 + */ +@JsonDeserialize(using = CallOriginInternalImpl.Deserializer.class) +public interface CallOrigin {} diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/package-info.java new file mode 100644 index 000000000..e32917b41 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/destination/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 call origins and destinations + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.destination; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/package-info.java new file mode 100644 index 000000000..fee906b41 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/package-info.java @@ -0,0 +1,6 @@ +/** + * Voice API interface V2 models + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Message.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Message.java new file mode 100644 index 000000000..7a32ec3a6 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Message.java @@ -0,0 +1,12 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.svaml.internal.MessageInternalImpl; + +/** + * Base interface for messages played by SVAML v2 commands. + * + * @since 2.3 + */ +@JsonDeserialize(using = MessageInternalImpl.Deserializer.class) +public interface Message {} diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlCommand.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlCommand.java new file mode 100644 index 000000000..abcafbe0c --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlCommand.java @@ -0,0 +1,12 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.svaml.internal.SvamlCommandInternalImpl; + +/** + * Base interface for SVAML v2 commands. + * + * @since 2.3 + */ +@JsonDeserialize(using = SvamlCommandInternalImpl.Deserializer.class) +public interface SvamlCommand {} diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/package-info.java new file mode 100644 index 000000000..d64f8200e --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 SVAML commands + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.svaml; diff --git a/client/src/main/com/sinch/sdk/models/VoiceContext.java b/client/src/main/com/sinch/sdk/models/VoiceContext.java index 780ac49ad..069cf7e0b 100644 --- a/client/src/main/com/sinch/sdk/models/VoiceContext.java +++ b/client/src/main/com/sinch/sdk/models/VoiceContext.java @@ -8,12 +8,22 @@ public class VoiceContext { private final VoiceRegion voiceRegion; private final String voiceUrl; private final String voiceApplicationManagementUrl; + private final String voiceV2Url; public VoiceContext( VoiceRegion voiceRegion, String voiceUrl, String voiceApplicationManagementUrl) { + this(voiceRegion, voiceUrl, voiceApplicationManagementUrl, null); + } + + public VoiceContext( + VoiceRegion voiceRegion, + String voiceUrl, + String voiceApplicationManagementUrl, + String voiceV2Url) { this.voiceRegion = voiceRegion; this.voiceUrl = voiceUrl; this.voiceApplicationManagementUrl = voiceApplicationManagementUrl; + this.voiceV2Url = voiceV2Url; } /** @@ -66,6 +76,26 @@ public ServerConfiguration getVoiceApplicationManagementServer() { return new ServerConfiguration(getVoiceApplicationManagementUrl()); } + /** + * Voice V2 URL + * + * @return Voice V2 Server URL + * @since 2.3 + */ + public String getVoiceV2Url() { + return voiceV2Url; + } + + /** + * Voice V2 Server Configuration + * + * @return Voice V2 Server configuration to be used + * @since 2.3 + */ + public ServerConfiguration getVoiceV2Server() { + return new ServerConfiguration(getVoiceV2Url()); + } + /** * Getting Builder * @@ -97,6 +127,7 @@ public static class Builder { VoiceRegion voiceRegion; String voiceUrl; String voiceApplicationMngmtUrl; + String voiceV2Url; protected Builder() {} @@ -111,6 +142,7 @@ protected Builder(VoiceContext context) { this.voiceUrl = null != context ? context.getVoiceUrl() : null; this.voiceApplicationMngmtUrl = null != context ? context.getVoiceApplicationManagementUrl() : null; + this.voiceV2Url = null != context ? context.getVoiceV2Url() : null; } /** @@ -149,6 +181,18 @@ public Builder setVoiceApplicationMngmtUrl(String voiceApplicationMngmtUrl) { return this; } + /** + * Set Voice V2 URL to be used + * + * @param voiceV2Url Voice V2 URL + * @return Current builder + * @since 2.3 + */ + public Builder setVoiceV2Url(String voiceV2Url) { + this.voiceV2Url = voiceV2Url; + return this; + } + /** * Create instance * @@ -157,7 +201,7 @@ public Builder setVoiceApplicationMngmtUrl(String voiceApplicationMngmtUrl) { */ public VoiceContext build() { - return new VoiceContext(voiceRegion, voiceUrl, voiceApplicationMngmtUrl); + return new VoiceContext(voiceRegion, voiceUrl, voiceApplicationMngmtUrl, voiceV2Url); } } } diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceIdempotencyKeyTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceIdempotencyKeyTest.java new file mode 100644 index 000000000..39ba98bb0 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceIdempotencyKeyTest.java @@ -0,0 +1,154 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.core.utils.Pair; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.DialCommandDtoTest; +import com.sinch.sdk.http.DefaultRetryManager; +import com.sinch.sdk.http.HttpClientApache; +import com.sinch.sdk.models.RetryConfiguration; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * The Idempotency-Key must be the same on every attempt the SDK makes for one call, whether the + * caller set it or the SDK generated it. Otherwise a retry after a rate limit or a token refresh + * could start a second call. + */ +class CallsServiceIdempotencyKeyTest { + + static final String START_CALL_RESPONSE = + "{\"projectId\": \"p\", \"serviceId\": \"s\", \"sessionId\": \"x\"}"; + + /** Stands in for OAuthManager: a bearer scheme, so an expired token triggers a retry. */ + static final AuthManager BEARER = + new AuthManager() { + @Override + public String getSchema() { + return AuthManager.SCHEMA_KEYWORD_BEARER; + } + + @Override + public void resetToken() {} + + @Override + public Collection> getAuthorizationHeaders( + String timestamp, String method, String httpContentType, String path, String body) { + return Collections.singletonList(new Pair<>("Authorization", "Bearer token")); + } + + @Override + public boolean validateAuthenticatedRequest( + String method, String path, Map headers, String jsonPayload) { + return false; + } + }; + + MockWebServer server; + HttpClientApache httpClient; + CallsService service; + + @BeforeEach + void setUp() throws IOException { + server = new MockWebServer(); + server.start(); + httpClient = + new HttpClientApache( + null, + new DefaultRetryManager(RetryConfiguration.builder().setMaxRetryCount(1).build())); + service = + new CallsServiceImpl( + httpClient, + new ServerConfiguration("http://" + server.getHostName() + ":" + server.getPort()), + Collections.singletonMap("SinchOAuth2", BEARER), + HttpMapper.getInstance(), + "project"); + } + + @AfterEach + void tearDown() throws Exception { + httpClient.close(); + server.shutdown(); + } + + StartCallRequest.Builder request() { + return StartCallRequest.builder() + .setCommands(Collections.singletonList(DialCommandDtoTest.expectedDialCommand)); + } + + MockResponse created() { + return new MockResponse() + .setResponseCode(201) + .addHeader("Content-Type", "application/json") + .setBody(START_CALL_RESPONSE); + } + + String takeIdempotencyKey() throws InterruptedException { + return server.takeRequest().getHeader("Idempotency-Key"); + } + + @Test + void generatedKeyIsReusedWhenRateLimitedCallIsRetried() throws Exception { + server.enqueue(new MockResponse().setResponseCode(429).addHeader("Retry-After", "1")); + server.enqueue(created()); + + service.start(request().build()); + + assertEquals(2, server.getRequestCount(), "expected the 429 to be retried once"); + String first = takeIdempotencyKey(); + assertDoesNotThrow(() -> UUID.fromString(first), "generated key is not a UUID: " + first); + assertEquals(first, takeIdempotencyKey()); + } + + @Test + void callerKeyIsReusedWhenRateLimitedCallIsRetried() throws Exception { + server.enqueue(new MockResponse().setResponseCode(429).addHeader("Retry-After", "1")); + server.enqueue(created()); + + service.start(request().setIdempotencyKey("caller-key-0123456789").build()); + + assertEquals(2, server.getRequestCount(), "expected the 429 to be retried once"); + assertEquals("caller-key-0123456789", takeIdempotencyKey()); + assertEquals("caller-key-0123456789", takeIdempotencyKey()); + } + + @Test + void generatedKeyIsReusedWhenCallIsRetriedAfterTokenRefresh() throws Exception { + server.enqueue( + new MockResponse() + .setResponseCode(401) + .addHeader("www-authenticate", "Bearer error=\"expired\"")); + server.enqueue(created()); + + service.start(request().build()); + + assertEquals(2, server.getRequestCount(), "expected the 401 to be retried once"); + assertEquals(takeIdempotencyKey(), takeIdempotencyKey()); + } + + @Test + void eachCallGetsItsOwnGeneratedKey() throws Exception { + server.enqueue(created()); + server.enqueue(created()); + + service.start(request().build()); + service.start(request().build()); + + assertNotEquals(takeIdempotencyKey(), takeIdempotencyKey()); + } +} diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java new file mode 100644 index 000000000..46cfc66dc --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java @@ -0,0 +1,162 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpContentType; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.HttpRequestTest.HttpRequestMatcher; +import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.URLParameter; +import com.sinch.sdk.core.http.URLPathUtils; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; +import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponseDtoTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.DialCommandDtoTest; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; + +@TestWithResources +public class CallsServiceTest extends BaseTest { + + @Mock HttpClient httpClient; + @Mock ServerConfiguration serverConfiguration; + @Mock Map authManagers; + + static final String PROJECT_ID = "test_project_id"; + static final Collection AUTH_NAMES = Arrays.asList("BasicAuth", "SinchOAuth2"); + static final Collection ACCEPTS = + Arrays.asList(HttpContentType.APPLICATION_JSON, "application/problem+json"); + + CallsService service; + + @GivenTextResource("/domains/voice/v2/calls/request/StartCallRequestDto.json") + String jsonStartCallRequestDto; + + @GivenTextResource("/domains/voice/v2/calls/response/StartCallResponseDto.json") + String jsonStartCallResponseDto; + + @BeforeEach + public void initMocks() { + service = + new CallsServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), PROJECT_ID); + } + + @Test + void start() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/calls", + HttpMethod.POST, + Collections.singletonList( + new URLParameter( + "serviceId", + StartCallRequestDtoTest.expectedStartCallRequest.getServiceId(), + URLParameter.form, + true)), + jsonStartCallRequestDto, + Collections.singletonMap( + "Idempotency-Key", + StartCallRequestDtoTest.expectedStartCallRequest.getIdempotencyKey()), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(201, null, Collections.emptyMap(), jsonStartCallResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + StartCallResponse response = service.start(StartCallRequestDtoTest.expectedStartCallRequest); + + TestHelpers.recursiveEquals(response, StartCallResponseDtoTest.expectedStartCallResponse); + } + + @Test + void startWithoutIdempotencyKeyGeneratesOne() throws ApiException { + + HttpResponse httpResponse = + new HttpResponse(201, null, Collections.emptyMap(), jsonStartCallResponseDto.getBytes()); + + when(httpClient.invokeAPI(eq(serverConfiguration), eq(authManagers), any(HttpRequest.class))) + .thenReturn(httpResponse); + + StartCallResponse response = + service.start( + StartCallRequest.builder() + .setCommands(Arrays.asList(DialCommandDtoTest.expectedDialCommand)) + .build()); + + ArgumentCaptor sent = ArgumentCaptor.forClass(HttpRequest.class); + verify(httpClient).invokeAPI(eq(serverConfiguration), eq(authManagers), sent.capture()); + + String idempotencyKey = sent.getValue().getHeaderParams().get("Idempotency-Key"); + Assertions.assertDoesNotThrow( + () -> UUID.fromString(idempotencyKey), "generated key is not a UUID: " + idempotencyKey); + + HttpRequest expected = + new HttpRequest( + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/calls", + HttpMethod.POST, + Collections.emptyList(), + jsonStartCallRequestDto, + Collections.singletonMap("Idempotency-Key", idempotencyKey), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + Assertions.assertTrue(new HttpRequestMatcher(expected).matches(sent.getValue())); + + TestHelpers.recursiveEquals(response, StartCallResponseDtoTest.expectedStartCallResponse); + } + + @Test + void startMissingRequestThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.start(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void startMissingProjectIdThrows() { + + CallsService serviceWithoutProjectId = + new CallsServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> serviceWithoutProjectId.start(StartCallRequestDtoTest.expectedStartCallRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } +} diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java new file mode 100644 index 000000000..08b9d99d7 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java @@ -0,0 +1,90 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.models.UnifiedCredentials; +import com.sinch.sdk.models.VoiceContext; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +class VoiceServiceTest { + + @Mock HttpClient httpClient; + + static final ServerConfiguration oAuthServer = new ServerConfiguration("https://oauth.foo.url"); + static final VoiceContext context = VoiceContext.builder().setVoiceV2Url("foo url").build(); + + static UnifiedCredentials credentials(String keyId, String keySecret, String projectId) { + return UnifiedCredentials.builder() + .setKeyId(keyId) + .setKeySecret(keySecret) + .setProjectId(projectId) + .build(); + } + + VoiceService service(UnifiedCredentials credentials, VoiceContext context) { + return new VoiceService(credentials, context, oAuthServer, () -> httpClient); + } + + @Test + void doNotAcceptNullCredentials() { + Exception exception = + assertThrows(NullPointerException.class, () -> service(null, context).calls()); + assertTrue(exception.getMessage().contains("Voice V2 service requires unified credentials")); + } + + @Test + void doNotAcceptNullKey() { + Exception exception = + assertThrows( + IllegalArgumentException.class, + () -> service(credentials(null, "foo", "foo"), context).calls()); + assertTrue(exception.getMessage().contains("keyId")); + } + + @Test + void doNotAcceptNullKeySecret() { + Exception exception = + assertThrows( + IllegalArgumentException.class, + () -> service(credentials("foo", null, "foo"), context).calls()); + assertTrue(exception.getMessage().contains("keySecret")); + } + + @Test + void doNotAcceptNullProject() { + Exception exception = + assertThrows( + IllegalArgumentException.class, + () -> service(credentials("foo", "foo", null), context).calls()); + assertTrue(exception.getMessage().contains("projectId")); + } + + @Test + void doNotAcceptNullContext() { + Exception exception = + assertThrows( + NullPointerException.class, + () -> service(credentials("foo", "foo", "foo"), null).calls()); + assertTrue(exception.getMessage().contains("Voice V2 service requires context")); + } + + @Test + void doNotAcceptNullVoiceV2Url() { + Exception exception = + assertThrows( + IllegalArgumentException.class, + () -> + service(credentials("foo", "foo", "foo"), VoiceContext.builder().build()).calls()); + assertTrue(exception.getMessage().contains("voiceV2Url")); + } + + @Test + void initPassed() { + assertDoesNotThrow(() -> service(credentials("foo", "foo", "foo"), context).calls()); + } +} diff --git a/client/src/test/java/com/sinch/sdk/e2e/Config.java b/client/src/test/java/com/sinch/sdk/e2e/Config.java index dd7c2ab44..e47f9251a 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/Config.java +++ b/client/src/test/java/com/sinch/sdk/e2e/Config.java @@ -32,6 +32,7 @@ public class Config { public static final String VOICE_HOST_NAME = MOCK_SERVER_URL + "/voice"; public static final String VOICE_MANAGEMENT_HOST_NAME = MOCK_SERVER_URL + "/voice-application-management"; + public static final String VOICE_V2_HOST_NAME = MOCK_SERVER_URL + "/voice-v2"; public static final String MAILGUN_HOST_NAME = MOCK_SERVER_URL + "/mailgun"; public static final String MAILGUN_API_KEY = "apiKey"; @@ -80,6 +81,7 @@ private Config() { VoiceContext.builder() .setVoiceApplicationMngmtUrl(VOICE_MANAGEMENT_HOST_NAME) .setVoiceUrl(VOICE_HOST_NAME) + .setVoiceV2Url(VOICE_V2_HOST_NAME) .build()) .setSmsContext( SmsContext.builder().setSmsUrl(SMS_HOST_NAME).setSmsRegion(SMSRegion.EU).build()) diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java new file mode 100644 index 000000000..a9f31ccf9 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java @@ -0,0 +1,99 @@ +package com.sinch.sdk.e2e.domains.voice.v2; + +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; +import com.sinch.sdk.domains.voice.models.v2.destination.Phone; +import com.sinch.sdk.domains.voice.models.v2.destination.PhoneDetails; +import com.sinch.sdk.domains.voice.models.v2.svaml.CallEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.DialCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.HangupCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.MessageEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.MessagesCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.Play; +import com.sinch.sdk.domains.voice.models.v2.svaml.PlayMessage; +import com.sinch.sdk.domains.voice.models.v2.svaml.Say; +import com.sinch.sdk.domains.voice.models.v2.svaml.SayMessage; +import com.sinch.sdk.e2e.Config; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Assertions; + +public class CallsSteps { + + CallsService service; + StartCallResponse startResponse; + + @Given("^the Voice-V2 service \"Calls\" is available$") + public void serviceAvailable() { + service = Config.getSinchClient().voice().v2().calls(); + Assertions.assertNotNull(service, "Voice V2 Calls service is not available"); + } + + @When("^I send a request to start a call$") + public void start() { + MessagesCommand notification = + MessagesCommand.builder() + .setMessagesName("notification") + .setMessages( + Arrays.asList( + PlayMessage.builder() + .setPlay( + Play.builder() + .setUrl("https://samplelib.com/mp3/sample-12s.mp3") + .build()) + .build(), + SayMessage.builder() + .setSay( + Say.builder() + .setText( + "Hello! This is a test notification from Sinch. Your" + + " verification code is 4 8 3 7.") + .setVoiceName("Emma") + .build()) + .build())) + .setEvents( + MessageEvents.builder() + .setOnFinish(Collections.singletonList(HangupCommand.builder().build())) + .build()) + .build(); + + DialCommand dial = + DialCommand.builder() + .setCallName("audio-notification") + .setFrom( + Phone.builder() + .setPhone(PhoneDetails.builder().setNumber("+12015555555").build()) + .build()) + .setTo( + Phone.builder() + .setPhone(PhoneDetails.builder().setNumber("+12017777777").build()) + .build()) + .setDialTimeoutDurationSeconds(30) + .setMaxCallDurationSeconds(300) + .setEvents( + CallEvents.builder().setOnAnswer(Collections.singletonList(notification)).build()) + .build(); + + // No Idempotency-Key set: the mock requires one, so this also checks the SDK generates it. + startResponse = + service.start( + StartCallRequest.builder().setCommands(Collections.singletonList(dial)).build()); + } + + @Then("^the response contains the information about the call started$") + public void startResult() { + StartCallResponse expected = + StartCallResponse.builder() + .setProjectId("a1b2c3d4-e5f6-4789-a012-b3c4d5e6f789") + .setServiceId("f9e8d7c6-b5a4-4321-9876-c5d4e3f2a1b0") + .setSessionId("01HZXK7QNPMR8VD3JW9YF2C4TB") + .build(); + + TestHelpers.recursiveEquals(startResponse, expected); + } +} diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceV2IT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceV2IT.java new file mode 100644 index 000000000..49371bc2f --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceV2IT.java @@ -0,0 +1,24 @@ +package com.sinch.sdk.e2e.domains.voice.v2; + +import static io.cucumber.junit.platform.engine.Constants.FILTER_NAME_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME; + +import org.junit.platform.suite.api.ConfigurationParameter; +import org.junit.platform.suite.api.IncludeEngines; +import org.junit.platform.suite.api.SelectClasspathResource; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +/** + * Only the scenarios of the operations this SDK implements. Widen the resource and the name filter + * as the other Voice V2 operations land. + */ +@Suite +@SuiteDisplayName("Voice V2") +@IncludeEngines("cucumber") +@SelectClasspathResource("features/voice-v2/calls.feature") +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.sinch.sdk.e2e.domains.voice.v2") +@ConfigurationParameter(key = FILTER_NAME_PROPERTY_NAME, value = "^\\[Start\\].*") +@ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") +public class VoiceV2IT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 4b7569734..09040eb0a 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -21,3 +21,6 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/applications/UnAssignNumber](./applications/UnAssignNumber.java) - [voice/applications/GetEventDestinations](./applications/GetEventDestinations.java) - [voice/applications/UpdateEventDestinations](./applications/UpdateEventDestinations.java) +- V2 + - Calls + - [voice/v2/calls/Start](./v2/calls/Start.java) diff --git a/examples/snippets/src/main/java/voice/v2/calls/Start.java b/examples/snippets/src/main/java/voice/v2/calls/Start.java new file mode 100644 index 000000000..5f1680b44 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/calls/Start.java @@ -0,0 +1,94 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.calls; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; +import com.sinch.sdk.domains.voice.models.v2.destination.Phone; +import com.sinch.sdk.domains.voice.models.v2.destination.PhoneDetails; +import com.sinch.sdk.domains.voice.models.v2.svaml.CallEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.DialCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.HangupCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.MessagesCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.Say; +import com.sinch.sdk.domains.voice.models.v2.svaml.SayMessage; +import com.sinch.sdk.models.Configuration; +import java.util.Collections; +import java.util.logging.Logger; +import utils.Settings; + +public class Start { + + private static final Logger LOGGER = Logger.getLogger(Start.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The phone number to be used as the caller ID, in E.164 format (e.g., +12025550123) + String sinchPhoneNumber = Settings.getPhoneNumber().orElse("MY_SINCH_PHONE_NUMBER"); + // The phone number you want to call, in E.164 format (e.g., +12025550123) + String recipientPhoneNumber = "RECIPIENT_PHONE_NUMBER"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + CallsService callsService = client.voice().v2().calls(); + + // Played to the recipient once the call is answered + MessagesCommand greeting = + MessagesCommand.builder() + .setMessages( + Collections.singletonList( + SayMessage.builder() + .setSay( + Say.builder() + .setText("Hello, your call is now connected.") + .setVoiceName("Emma") + .build()) + .build())) + .build(); + + DialCommand dial = + DialCommand.builder() + .setCallName("Java_SDK_Snippet_Call") + .setFrom( + Phone.builder() + .setPhone(PhoneDetails.builder().setNumber(sinchPhoneNumber).build()) + .build()) + .setTo( + Phone.builder() + .setPhone(PhoneDetails.builder().setNumber(recipientPhoneNumber).build()) + .build()) + .setEvents( + CallEvents.builder() + .setOnAnswer(Collections.singletonList(greeting)) + .setOnHangup(Collections.singletonList(HangupCommand.builder().build())) + .build()) + .build(); + + StartCallRequest request = + StartCallRequest.builder().setCommands(Collections.singletonList(dial)).build(); + + LOGGER.info("Start a call to: " + recipientPhoneNumber); + + StartCallResponse response = callsService.start(request); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java new file mode 100644 index 000000000..e9d54627e --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java @@ -0,0 +1,23 @@ +package com.sinch.sdk.domains.voice.api.v2; + +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; + +/** Calls Service */ +public interface CallsService { + + /** + * Create and initiate a new outbound voice call + * + *

Create a new outbound call associated to the project's default service or to the service + * specified by {@link StartCallRequest#getServiceId()}. + * + * @param startCallRequest The SVAML commands describing the call flow, plus the optional service + * ID and idempotency key (required) + * @return StartCallResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + StartCallResponse start(StartCallRequest startCallRequest) throws ApiException; +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java new file mode 100644 index 000000000..3a981eb75 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java @@ -0,0 +1,131 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.exceptions.ApiExceptionBuilder; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.HttpStatus; +import com.sinch.sdk.core.http.URLParameter; +import com.sinch.sdk.core.http.URLParameterUtils; +import com.sinch.sdk.core.http.URLPathUtils; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.core.utils.StringUtil; +import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.Logger; + +public class CallsServiceImpl implements com.sinch.sdk.domains.voice.api.v2.CallsService { + + private static final Logger LOGGER = Logger.getLogger(CallsServiceImpl.class.getName()); + private final HttpClient httpClient; + private final ServerConfiguration serverConfiguration; + private final Map authManagersByOasSecuritySchemes; + private final HttpMapper mapper; + + private final String projectId; + + public CallsServiceImpl( + HttpClient httpClient, + ServerConfiguration serverConfiguration, + Map authManagersByOasSecuritySchemes, + HttpMapper mapper, + String projectId) { + this.httpClient = httpClient; + this.serverConfiguration = serverConfiguration; + this.authManagersByOasSecuritySchemes = authManagersByOasSecuritySchemes; + this.mapper = mapper; + this.projectId = projectId; + } + + @Override + public StartCallResponse start(StartCallRequest startCallRequest) throws ApiException { + + LOGGER.finest("[start]" + " " + "startCallRequest: " + startCallRequest); + + HttpRequest httpRequest = startRequestBuilder(startCallRequest); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest startRequestBuilder(StartCallRequest startCallRequest) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling start"); + } + // verify the required parameter 'startCallRequest' is set + if (startCallRequest == null) { + throw new ApiException( + 400, "Missing the required parameter 'startCallRequest' when calling start"); + } + + String localVarPath = + "/v2/projects/{projectId}/calls" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())); + + List localVarQueryParams = new ArrayList<>(); + if (null != startCallRequest.getServiceId()) { + URLParameterUtils.addQueryParam( + OptionalValue.of(startCallRequest.getServiceId()), + "serviceId", + URLParameter.form, + null, + localVarQueryParams, + true); + } + + // Without a key from the caller, one is generated here, once per call. The HTTP client + // re-sends this same request on every retry, so the key does not change between attempts. + String idempotencyKey = startCallRequest.getIdempotencyKey(); + if (StringUtil.isEmpty(idempotencyKey)) { + idempotencyKey = UUID.randomUUID().toString(); + } + Map localVarHeaderParams = new HashMap<>(); + localVarHeaderParams.put("Idempotency-Key", idempotencyKey); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList("application/json"); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = mapper.serialize(localVarContentTypes, startCallRequest); + + return new HttpRequest( + localVarPath, + HttpMethod.POST, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java new file mode 100644 index 000000000..a3bb68378 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java @@ -0,0 +1,101 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand; +import java.util.List; + +/** Request to start a single outbound call. */ +@JsonDeserialize(builder = StartCallRequestImpl.Builder.class) +public interface StartCallRequest { + + /** + * An ordered list of SVAML v2 (Sinch Voice Application Markup Language) commands that describe a + * call flow. Commands are executed sequentially in the order they are defined. + * + *

Blocking vs. non-blocking: Some commands block execution until they complete (pause + * , webhook, menu, gotoMenu), while others return + * immediately and run in parallel (dial, messages, amd, + * answer, hangup, startRecording, stopRecording + * , bridgeCall, stopMessages). Each command's description + * specifies its behavior. + * + *

Nesting scope: Commands that appear inside event handlers (e.g., dial.events.onAnswer + * , messages.events.onFinish) form independent sequences and execute in their + * own context — they are not continuations of the parent sequence. + * + *

Field is required + * + * @return commands + */ + List getCommands(); + + /** + * The ID of the service to use for the call. If omitted, the project's default service is used. + * + *

Sent as the serviceId query parameter. + * + * @return serviceId + */ + String getServiceId(); + + /** + * Client-generated idempotency key to safely retry requests. The server uses this key to + * recognize retries of the same request. If a request with the same key is received within 10 + * minutes, the server returns the cached response from the original request. Using a random UUID + * (v4) is strongly recommended. + * + *

Sent as the Idempotency-Key header. If not set, the SDK generates a random UUID + * for each call. Whether set or generated, the same key is sent on every automatic retry of that + * call. + * + * @return idempotencyKey + */ + String getIdempotencyKey(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StartCallRequestImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param commands see getter + * @return Current builder + * @see #getCommands + */ + Builder setCommands(List commands); + + /** + * see getter + * + * @param serviceId see getter + * @return Current builder + * @see #getServiceId + */ + Builder setServiceId(String serviceId); + + /** + * see getter + * + * @param idempotencyKey see getter + * @return Current builder + * @see #getIdempotencyKey + */ + Builder setIdempotencyKey(String idempotencyKey); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + StartCallRequest build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestImpl.java new file mode 100644 index 000000000..a6a3dc879 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestImpl.java @@ -0,0 +1,139 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.request; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({StartCallRequestImpl.JSON_PROPERTY_COMMANDS}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StartCallRequestImpl implements StartCallRequest { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMANDS = "commands"; + + private OptionalValue> commands; + + private OptionalValue serviceId; + + private OptionalValue idempotencyKey; + + public StartCallRequestImpl() {} + + protected StartCallRequestImpl( + OptionalValue> commands, + OptionalValue serviceId, + OptionalValue idempotencyKey) { + this.commands = commands; + this.serviceId = serviceId; + this.idempotencyKey = idempotencyKey; + } + + @JsonIgnore + public List getCommands() { + return commands.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMANDS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> commands() { + return commands; + } + + @JsonIgnore + public String getServiceId() { + return serviceId.orElse(null); + } + + @JsonIgnore + public OptionalValue serviceId() { + return serviceId; + } + + @JsonIgnore + public String getIdempotencyKey() { + return idempotencyKey.orElse(null); + } + + @JsonIgnore + public OptionalValue idempotencyKey() { + return idempotencyKey; + } + + /** Return true if this StartCallRequest object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StartCallRequestImpl startCallRequest = (StartCallRequestImpl) o; + return Objects.equals(this.commands, startCallRequest.commands) + && Objects.equals(this.serviceId, startCallRequest.serviceId) + && Objects.equals(this.idempotencyKey, startCallRequest.idempotencyKey); + } + + @Override + public int hashCode() { + return Objects.hash(commands, serviceId, idempotencyKey); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StartCallRequestImpl {\n"); + sb.append(" commands: ").append(toIndentedString(commands)).append("\n"); + sb.append(" serviceId: ").append(toIndentedString(serviceId)).append("\n"); + sb.append(" idempotencyKey: ").append(toIndentedString(idempotencyKey)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements StartCallRequest.Builder { + OptionalValue> commands = OptionalValue.empty(); + OptionalValue serviceId = OptionalValue.empty(); + OptionalValue idempotencyKey = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMANDS, required = true) + public Builder setCommands(List commands) { + this.commands = OptionalValue.of(commands); + return this; + } + + @JsonIgnore + public Builder setServiceId(String serviceId) { + this.serviceId = OptionalValue.of(serviceId); + return this; + } + + @JsonIgnore + public Builder setIdempotencyKey(String idempotencyKey) { + this.idempotencyKey = OptionalValue.of(idempotencyKey); + return this; + } + + public StartCallRequest build() { + return new StartCallRequestImpl(commands, serviceId, idempotencyKey); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponse.java new file mode 100644 index 000000000..4be5954b4 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponse.java @@ -0,0 +1,86 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * Response returned after starting a single outbound call. + * + *

Contains the ID of the created call session and of the project and service it belongs to. + */ +@JsonDeserialize(builder = StartCallResponseImpl.Builder.class) +public interface StartCallResponse { + + /** + * The ID of the project. + * + *

Field is required + * + * @return projectId + */ + String getProjectId(); + + /** + * The ID of the service. + * + *

Field is required + * + * @return serviceId + */ + String getServiceId(); + + /** + * The ID of the call session. + * + *

Field is required + * + * @return sessionId + */ + String getSessionId(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StartCallResponseImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param projectId see getter + * @return Current builder + * @see #getProjectId + */ + Builder setProjectId(String projectId); + + /** + * see getter + * + * @param serviceId see getter + * @return Current builder + * @see #getServiceId + */ + Builder setServiceId(String serviceId); + + /** + * see getter + * + * @param sessionId see getter + * @return Current builder + * @see #getSessionId + */ + Builder setSessionId(String sessionId); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + StartCallResponse build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponseImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponseImpl.java new file mode 100644 index 000000000..c82fae680 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponseImpl.java @@ -0,0 +1,147 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + StartCallResponseImpl.JSON_PROPERTY_PROJECT_ID, + StartCallResponseImpl.JSON_PROPERTY_SERVICE_ID, + StartCallResponseImpl.JSON_PROPERTY_SESSION_ID +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StartCallResponseImpl implements StartCallResponse { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_PROJECT_ID = "projectId"; + + private OptionalValue projectId; + + public static final String JSON_PROPERTY_SERVICE_ID = "serviceId"; + + private OptionalValue serviceId; + + public static final String JSON_PROPERTY_SESSION_ID = "sessionId"; + + private OptionalValue sessionId; + + public StartCallResponseImpl() {} + + protected StartCallResponseImpl( + OptionalValue projectId, + OptionalValue serviceId, + OptionalValue sessionId) { + this.projectId = projectId; + this.serviceId = serviceId; + this.sessionId = sessionId; + } + + @JsonIgnore + public String getProjectId() { + return projectId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PROJECT_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue projectId() { + return projectId; + } + + @JsonIgnore + public String getServiceId() { + return serviceId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SERVICE_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue serviceId() { + return serviceId; + } + + @JsonIgnore + public String getSessionId() { + return sessionId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SESSION_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue sessionId() { + return sessionId; + } + + /** Return true if this StartCallResponse object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StartCallResponseImpl startCallResponse = (StartCallResponseImpl) o; + return Objects.equals(this.projectId, startCallResponse.projectId) + && Objects.equals(this.serviceId, startCallResponse.serviceId) + && Objects.equals(this.sessionId, startCallResponse.sessionId); + } + + @Override + public int hashCode() { + return Objects.hash(projectId, serviceId, sessionId); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StartCallResponseImpl {\n"); + sb.append(" projectId: ").append(toIndentedString(projectId)).append("\n"); + sb.append(" serviceId: ").append(toIndentedString(serviceId)).append("\n"); + sb.append(" sessionId: ").append(toIndentedString(sessionId)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements StartCallResponse.Builder { + OptionalValue projectId = OptionalValue.empty(); + OptionalValue serviceId = OptionalValue.empty(); + OptionalValue sessionId = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_PROJECT_ID, required = true) + public Builder setProjectId(String projectId) { + this.projectId = OptionalValue.of(projectId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SERVICE_ID, required = true) + public Builder setServiceId(String serviceId) { + this.serviceId = OptionalValue.of(serviceId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SESSION_ID, required = true) + public Builder setSessionId(String sessionId) { + this.sessionId = OptionalValue.of(sessionId); + return this; + } + + public StartCallResponse build() { + return new StartCallResponseImpl(projectId, serviceId, sessionId); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallHeader.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallHeader.java new file mode 100644 index 000000000..afe6d5ee9 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallHeader.java @@ -0,0 +1,62 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** Custom headers to be sent in the call setup. */ +@JsonDeserialize(builder = CallHeaderImpl.Builder.class) +public interface CallHeader { + + /** + * Name of the header. + * + *

Field is required + * + * @return key + */ + String getKey(); + + /** + * Value of the header. + * + * @return value + */ + String getValue(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new CallHeaderImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param key see getter + * @return Current builder + * @see #getKey + */ + Builder setKey(String key); + + /** + * see getter + * + * @param value see getter + * @return Current builder + * @see #getValue + */ + Builder setValue(String value); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + CallHeader build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallHeaderImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallHeaderImpl.java new file mode 100644 index 000000000..63318c12f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/CallHeaderImpl.java @@ -0,0 +1,114 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({CallHeaderImpl.JSON_PROPERTY_KEY, CallHeaderImpl.JSON_PROPERTY_VALUE}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class CallHeaderImpl implements CallHeader { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_KEY = "key"; + + private OptionalValue key; + + public static final String JSON_PROPERTY_VALUE = "value"; + + private OptionalValue value; + + public CallHeaderImpl() {} + + protected CallHeaderImpl(OptionalValue key, OptionalValue value) { + this.key = key; + this.value = value; + } + + @JsonIgnore + public String getKey() { + return key.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_KEY) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue key() { + return key; + } + + @JsonIgnore + public String getValue() { + return value.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_VALUE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue value() { + return value; + } + + /** Return true if this CallHeader object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CallHeaderImpl callHeader = (CallHeaderImpl) o; + return Objects.equals(this.key, callHeader.key) && Objects.equals(this.value, callHeader.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CallHeaderImpl {\n"); + sb.append(" key: ").append(toIndentedString(key)).append("\n"); + sb.append(" value: ").append(toIndentedString(value)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements CallHeader.Builder { + OptionalValue key = OptionalValue.empty(); + OptionalValue value = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_KEY, required = true) + public Builder setKey(String key) { + this.key = OptionalValue.of(key); + return this; + } + + @JsonProperty(JSON_PROPERTY_VALUE) + public Builder setValue(String value) { + this.value = OptionalValue.of(value); + return this; + } + + public CallHeader build() { + return new CallHeaderImpl(key, value); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Phone.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Phone.java new file mode 100644 index 000000000..740c5b9d1 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Phone.java @@ -0,0 +1,74 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; + +/** Routes the call to a phone number on the Public Switched Telephone Network (PSTN). */ +@JsonDeserialize(builder = PhoneImpl.Builder.class) +public interface Phone extends CallOrigin, CallDestination { + + /** The type property. Must have the value PHONE. */ + public class TypeEnum extends EnumDynamic { + /** The PHONE type. */ + public static final TypeEnum PHONE = new TypeEnum("PHONE"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(PHONE)); + + private TypeEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Get phone + * + *

Field is required + * + * @return phone + */ + PhoneDetails getPhone(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new PhoneImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param phone see getter + * @return Current builder + * @see #getPhone + */ + Builder setPhone(PhoneDetails phone); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + Phone build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetails.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetails.java new file mode 100644 index 000000000..a5ad69c69 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetails.java @@ -0,0 +1,45 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder = PhoneDetailsImpl.Builder.class) +public interface PhoneDetails { + + /** + * E.164 Phone number + * + *

Field is required + * + * @return number + */ + String getNumber(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new PhoneDetailsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param number see getter + * @return Current builder + * @see #getNumber + */ + Builder setNumber(String number); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + PhoneDetails build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetailsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetailsImpl.java new file mode 100644 index 000000000..49c320c9a --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetailsImpl.java @@ -0,0 +1,90 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({PhoneDetailsImpl.JSON_PROPERTY_NUMBER}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class PhoneDetailsImpl implements PhoneDetails { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_NUMBER = "number"; + + private OptionalValue number; + + public PhoneDetailsImpl() {} + + protected PhoneDetailsImpl(OptionalValue number) { + this.number = number; + } + + @JsonIgnore + public String getNumber() { + return number.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_NUMBER) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue number() { + return number; + } + + /** Return true if this PhoneDetails object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PhoneDetailsImpl phoneDetails = (PhoneDetailsImpl) o; + return Objects.equals(this.number, phoneDetails.number); + } + + @Override + public int hashCode() { + return Objects.hash(number); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PhoneDetailsImpl {\n"); + sb.append(" number: ").append(toIndentedString(number)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements PhoneDetails.Builder { + OptionalValue number = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_NUMBER, required = true) + public Builder setNumber(String number) { + this.number = OptionalValue.of(number); + return this; + } + + public PhoneDetails build() { + return new PhoneDetailsImpl(number); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneImpl.java new file mode 100644 index 000000000..789442e96 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/PhoneImpl.java @@ -0,0 +1,117 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({PhoneImpl.JSON_PROPERTY_TYPE, PhoneImpl.JSON_PROPERTY_PHONE}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class PhoneImpl implements Phone, CallOrigin, CallDestination { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_PHONE = "phone"; + + private OptionalValue phone; + + public PhoneImpl() {} + + protected PhoneImpl(OptionalValue type, OptionalValue phone) { + this.type = type; + this.phone = phone; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public PhoneDetails getPhone() { + return phone.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PHONE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue phone() { + return phone; + } + + /** Return true if this Phone object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PhoneImpl phone = (PhoneImpl) o; + return Objects.equals(this.type, phone.type) && Objects.equals(this.phone, phone.phone); + } + + @Override + public int hashCode() { + return Objects.hash(type, phone); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PhoneImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" phone: ").append(toIndentedString(phone)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements Phone.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.PHONE); + OptionalValue phone = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.PHONE)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.PHONE, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_PHONE, required = true) + public Builder setPhone(PhoneDetails phone) { + this.phone = OptionalValue.of(phone); + return this; + } + + public Phone build() { + return new PhoneImpl(type, phone); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Sip.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Sip.java new file mode 100644 index 000000000..963bef3c9 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Sip.java @@ -0,0 +1,73 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; + +@JsonDeserialize(builder = SipImpl.Builder.class) +public interface Sip extends CallDestination { + + /** The type property. Must have the value SIP. */ + public class TypeEnum extends EnumDynamic { + /** The SIP type. */ + public static final TypeEnum SIP = new TypeEnum("SIP"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(SIP)); + + private TypeEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Get sip + * + *

Field is required + * + * @return sip + */ + SipDetails getSip(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SipImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param sip see getter + * @return Current builder + * @see #getSip + */ + Builder setSip(SipDetails sip); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + Sip build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeader.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeader.java new file mode 100644 index 000000000..1ca1da21e --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeader.java @@ -0,0 +1,62 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** Custom SIP headers to be sent in the call setup. */ +@JsonDeserialize(builder = SipCallHeaderImpl.Builder.class) +public interface SipCallHeader { + + /** + * Name of the SIP header. + * + *

Field is required + * + * @return key + */ + String getKey(); + + /** + * Value of the SIP header. + * + * @return value + */ + String getValue(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SipCallHeaderImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param key see getter + * @return Current builder + * @see #getKey + */ + Builder setKey(String key); + + /** + * see getter + * + * @param value see getter + * @return Current builder + * @see #getValue + */ + Builder setValue(String value); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + SipCallHeader build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeaderImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeaderImpl.java new file mode 100644 index 000000000..15d5ccff3 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeaderImpl.java @@ -0,0 +1,115 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({SipCallHeaderImpl.JSON_PROPERTY_KEY, SipCallHeaderImpl.JSON_PROPERTY_VALUE}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SipCallHeaderImpl implements SipCallHeader { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_KEY = "key"; + + private OptionalValue key; + + public static final String JSON_PROPERTY_VALUE = "value"; + + private OptionalValue value; + + public SipCallHeaderImpl() {} + + protected SipCallHeaderImpl(OptionalValue key, OptionalValue value) { + this.key = key; + this.value = value; + } + + @JsonIgnore + public String getKey() { + return key.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_KEY) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue key() { + return key; + } + + @JsonIgnore + public String getValue() { + return value.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_VALUE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue value() { + return value; + } + + /** Return true if this SipCallHeader object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SipCallHeaderImpl sipCallHeader = (SipCallHeaderImpl) o; + return Objects.equals(this.key, sipCallHeader.key) + && Objects.equals(this.value, sipCallHeader.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SipCallHeaderImpl {\n"); + sb.append(" key: ").append(toIndentedString(key)).append("\n"); + sb.append(" value: ").append(toIndentedString(value)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements SipCallHeader.Builder { + OptionalValue key = OptionalValue.empty(); + OptionalValue value = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_KEY, required = true) + public Builder setKey(String key) { + this.key = OptionalValue.of(key); + return this; + } + + @JsonProperty(JSON_PROPERTY_VALUE) + public Builder setValue(String value) { + this.value = OptionalValue.of(value); + return this; + } + + public SipCallHeader build() { + return new SipCallHeaderImpl(key, value); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipDetails.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipDetails.java new file mode 100644 index 000000000..2ccb1998a --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipDetails.java @@ -0,0 +1,116 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.List; + +@JsonDeserialize(builder = SipDetailsImpl.Builder.class) +public interface SipDetails { + + /** Gets or Sets transport */ + public class TransportEnum extends EnumDynamic { + public static final TransportEnum UDP = new TransportEnum("UDP"); + + public static final TransportEnum TCP = new TransportEnum("TCP"); + + public static final TransportEnum TLS = new TransportEnum("TLS"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + TransportEnum.class, TransportEnum::new, Arrays.asList(UDP, TCP, TLS)); + + private TransportEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TransportEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TransportEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * SIP URI of the destination endpoint. Both sip: (unencrypted) and sips: + * (TLS-encrypted) schemes are supported. + * + *

Field is required + * + * @return endpoint + */ + String getEndpoint(); + + /** + * Transport protocol to use for the SIP signalling channel. + * + *

If omitted, the platform selects a default based on the URI scheme: UDP for + * sip: and TLS for sips:. Setting this explicitly + * overrides that default — for example, to force TCP for a sip: URI or + * to use TLS without switching to the sips: scheme. + * + * @return transport + */ + TransportEnum getTransport(); + + /** + * Custom SIP headers to be sent in the call setup. + * + * @return callHeaders + */ + List getCallHeaders(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SipDetailsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param endpoint see getter + * @return Current builder + * @see #getEndpoint + */ + Builder setEndpoint(String endpoint); + + /** + * see getter + * + * @param transport see getter + * @return Current builder + * @see #getTransport + */ + Builder setTransport(TransportEnum transport); + + /** + * see getter + * + * @param callHeaders see getter + * @return Current builder + * @see #getCallHeaders + */ + Builder setCallHeaders(List callHeaders); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + SipDetails build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipDetailsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipDetailsImpl.java new file mode 100644 index 000000000..811c476c7 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipDetailsImpl.java @@ -0,0 +1,148 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + SipDetailsImpl.JSON_PROPERTY_ENDPOINT, + SipDetailsImpl.JSON_PROPERTY_TRANSPORT, + SipDetailsImpl.JSON_PROPERTY_CALL_HEADERS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SipDetailsImpl implements SipDetails { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ENDPOINT = "endpoint"; + + private OptionalValue endpoint; + + public static final String JSON_PROPERTY_TRANSPORT = "transport"; + + private OptionalValue transport; + + public static final String JSON_PROPERTY_CALL_HEADERS = "callHeaders"; + + private OptionalValue> callHeaders; + + public SipDetailsImpl() {} + + protected SipDetailsImpl( + OptionalValue endpoint, + OptionalValue transport, + OptionalValue> callHeaders) { + this.endpoint = endpoint; + this.transport = transport; + this.callHeaders = callHeaders; + } + + @JsonIgnore + public String getEndpoint() { + return endpoint.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ENDPOINT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue endpoint() { + return endpoint; + } + + @JsonIgnore + public TransportEnum getTransport() { + return transport.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TRANSPORT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue transport() { + return transport; + } + + @JsonIgnore + public List getCallHeaders() { + return callHeaders.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_HEADERS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> callHeaders() { + return callHeaders; + } + + /** Return true if this SipDetails object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SipDetailsImpl sipDetails = (SipDetailsImpl) o; + return Objects.equals(this.endpoint, sipDetails.endpoint) + && Objects.equals(this.transport, sipDetails.transport) + && Objects.equals(this.callHeaders, sipDetails.callHeaders); + } + + @Override + public int hashCode() { + return Objects.hash(endpoint, transport, callHeaders); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SipDetailsImpl {\n"); + sb.append(" endpoint: ").append(toIndentedString(endpoint)).append("\n"); + sb.append(" transport: ").append(toIndentedString(transport)).append("\n"); + sb.append(" callHeaders: ").append(toIndentedString(callHeaders)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements SipDetails.Builder { + OptionalValue endpoint = OptionalValue.empty(); + OptionalValue transport = OptionalValue.empty(); + OptionalValue> callHeaders = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_ENDPOINT, required = true) + public Builder setEndpoint(String endpoint) { + this.endpoint = OptionalValue.of(endpoint); + return this; + } + + @JsonProperty(JSON_PROPERTY_TRANSPORT) + public Builder setTransport(TransportEnum transport) { + this.transport = OptionalValue.of(transport); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_HEADERS) + public Builder setCallHeaders(List callHeaders) { + this.callHeaders = OptionalValue.of(callHeaders); + return this; + } + + public SipDetails build() { + return new SipDetailsImpl(endpoint, transport, callHeaders); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFrom.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFrom.java new file mode 100644 index 000000000..247617bfa --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFrom.java @@ -0,0 +1,73 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; + +@JsonDeserialize(builder = SipFromImpl.Builder.class) +public interface SipFrom extends CallOrigin { + + /** The type property. Must have the value SIP. */ + public class TypeEnum extends EnumDynamic { + /** The SIP type. */ + public static final TypeEnum SIP = new TypeEnum("SIP"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(SIP)); + + private TypeEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Get sip + * + *

Field is required + * + * @return sip + */ + SipFromDetails getSip(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SipFromImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param sip see getter + * @return Current builder + * @see #getSip + */ + Builder setSip(SipFromDetails sip); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + SipFrom build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetails.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetails.java new file mode 100644 index 000000000..85c19c6f0 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetails.java @@ -0,0 +1,64 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder = SipFromDetailsImpl.Builder.class) +public interface SipFromDetails { + + /** + * SIP URI of the originating endpoint. Both sip: (unencrypted) and sips: + * (TLS-encrypted) schemes are supported. + * + *

Field is required + * + * @return endpoint + */ + String getEndpoint(); + + /** + * Display name presented to the called party as the caller identity. Transmitted as the display + * name part of the SIP From header (for example, + * Alice <sip:alice@example.com>). + * + * @return displayName + */ + String getDisplayName(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SipFromDetailsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param endpoint see getter + * @return Current builder + * @see #getEndpoint + */ + Builder setEndpoint(String endpoint); + + /** + * see getter + * + * @param displayName see getter + * @return Current builder + * @see #getDisplayName + */ + Builder setDisplayName(String displayName); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + SipFromDetails build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetailsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetailsImpl.java new file mode 100644 index 000000000..a15322f30 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetailsImpl.java @@ -0,0 +1,118 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + SipFromDetailsImpl.JSON_PROPERTY_ENDPOINT, + SipFromDetailsImpl.JSON_PROPERTY_DISPLAY_NAME +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SipFromDetailsImpl implements SipFromDetails { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ENDPOINT = "endpoint"; + + private OptionalValue endpoint; + + public static final String JSON_PROPERTY_DISPLAY_NAME = "displayName"; + + private OptionalValue displayName; + + public SipFromDetailsImpl() {} + + protected SipFromDetailsImpl(OptionalValue endpoint, OptionalValue displayName) { + this.endpoint = endpoint; + this.displayName = displayName; + } + + @JsonIgnore + public String getEndpoint() { + return endpoint.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ENDPOINT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue endpoint() { + return endpoint; + } + + @JsonIgnore + public String getDisplayName() { + return displayName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DISPLAY_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue displayName() { + return displayName; + } + + /** Return true if this SipFromDetails object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SipFromDetailsImpl sipFromDetails = (SipFromDetailsImpl) o; + return Objects.equals(this.endpoint, sipFromDetails.endpoint) + && Objects.equals(this.displayName, sipFromDetails.displayName); + } + + @Override + public int hashCode() { + return Objects.hash(endpoint, displayName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SipFromDetailsImpl {\n"); + sb.append(" endpoint: ").append(toIndentedString(endpoint)).append("\n"); + sb.append(" displayName: ").append(toIndentedString(displayName)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements SipFromDetails.Builder { + OptionalValue endpoint = OptionalValue.empty(); + OptionalValue displayName = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_ENDPOINT, required = true) + public Builder setEndpoint(String endpoint) { + this.endpoint = OptionalValue.of(endpoint); + return this; + } + + @JsonProperty(JSON_PROPERTY_DISPLAY_NAME) + public Builder setDisplayName(String displayName) { + this.displayName = OptionalValue.of(displayName); + return this; + } + + public SipFromDetails build() { + return new SipFromDetailsImpl(endpoint, displayName); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromImpl.java new file mode 100644 index 000000000..d13f7dad7 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipFromImpl.java @@ -0,0 +1,117 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({SipFromImpl.JSON_PROPERTY_TYPE, SipFromImpl.JSON_PROPERTY_SIP}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SipFromImpl implements SipFrom, CallOrigin { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_SIP = "sip"; + + private OptionalValue sip; + + public SipFromImpl() {} + + protected SipFromImpl(OptionalValue type, OptionalValue sip) { + this.type = type; + this.sip = sip; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public SipFromDetails getSip() { + return sip.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SIP) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue sip() { + return sip; + } + + /** Return true if this SipFrom object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SipFromImpl sipFrom = (SipFromImpl) o; + return Objects.equals(this.type, sipFrom.type) && Objects.equals(this.sip, sipFrom.sip); + } + + @Override + public int hashCode() { + return Objects.hash(type, sip); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SipFromImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" sip: ").append(toIndentedString(sip)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements SipFrom.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.SIP); + OptionalValue sip = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.SIP)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.SIP, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SIP, required = true) + public Builder setSip(SipFromDetails sip) { + this.sip = OptionalValue.of(sip); + return this; + } + + public SipFrom build() { + return new SipFromImpl(type, sip); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipImpl.java new file mode 100644 index 000000000..bc1ce973b --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/SipImpl.java @@ -0,0 +1,117 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({SipImpl.JSON_PROPERTY_TYPE, SipImpl.JSON_PROPERTY_SIP}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SipImpl implements Sip, CallDestination { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_SIP = "sip"; + + private OptionalValue sip; + + public SipImpl() {} + + protected SipImpl(OptionalValue type, OptionalValue sip) { + this.type = type; + this.sip = sip; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public SipDetails getSip() { + return sip.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SIP) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue sip() { + return sip; + } + + /** Return true if this Sip object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SipImpl sip = (SipImpl) o; + return Objects.equals(this.type, sip.type) && Objects.equals(this.sip, sip.sip); + } + + @Override + public int hashCode() { + return Objects.hash(type, sip); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SipImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" sip: ").append(toIndentedString(sip)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements Sip.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.SIP); + OptionalValue sip = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.SIP)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.SIP, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SIP, required = true) + public Builder setSip(SipDetails sip) { + this.sip = OptionalValue.of(sip); + return this; + } + + public Sip build() { + return new SipImpl(type, sip); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Stream.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Stream.java new file mode 100644 index 000000000..562687c87 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/Stream.java @@ -0,0 +1,74 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; + +/** Routes the call to a WebSocket stream endpoint for real-time audio processing. */ +@JsonDeserialize(builder = StreamImpl.Builder.class) +public interface Stream extends CallDestination { + + /** The type property. Must have the value STREAM. */ + public class TypeEnum extends EnumDynamic { + /** The STREAM type. */ + public static final TypeEnum STREAM = new TypeEnum("STREAM"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(STREAM)); + + private TypeEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Get stream + * + *

Field is required + * + * @return stream + */ + StreamDetails getStream(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StreamImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param stream see getter + * @return Current builder + * @see #getStream + */ + Builder setStream(StreamDetails stream); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + Stream build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetails.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetails.java new file mode 100644 index 000000000..338796262 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetails.java @@ -0,0 +1,81 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +@JsonDeserialize(builder = StreamDetailsImpl.Builder.class) +public interface StreamDetails { + + /** + * WebSocket endpoint that will accept the incoming connection for real-time audio streaming. Must + * be a valid WebSocket URL using either ws:// or wss:// (recommended). + * The URL must be reachable from the public internet and capable of handling the negotiated + * stream protocol. + * + *

Field is required + * + * @return endpoint + */ + String getEndpoint(); + + /** + * Get streamOptions + * + * @return streamOptions + */ + StreamOptions getStreamOptions(); + + /** + * Custom headers to be sent in the call setup. + * + * @return callHeaders + */ + List getCallHeaders(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StreamDetailsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param endpoint see getter + * @return Current builder + * @see #getEndpoint + */ + Builder setEndpoint(String endpoint); + + /** + * see getter + * + * @param streamOptions see getter + * @return Current builder + * @see #getStreamOptions + */ + Builder setStreamOptions(StreamOptions streamOptions); + + /** + * see getter + * + * @param callHeaders see getter + * @return Current builder + * @see #getCallHeaders + */ + Builder setCallHeaders(List callHeaders); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + StreamDetails build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetailsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetailsImpl.java new file mode 100644 index 000000000..0e4720dc3 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetailsImpl.java @@ -0,0 +1,148 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + StreamDetailsImpl.JSON_PROPERTY_ENDPOINT, + StreamDetailsImpl.JSON_PROPERTY_STREAM_OPTIONS, + StreamDetailsImpl.JSON_PROPERTY_CALL_HEADERS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StreamDetailsImpl implements StreamDetails { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ENDPOINT = "endpoint"; + + private OptionalValue endpoint; + + public static final String JSON_PROPERTY_STREAM_OPTIONS = "streamOptions"; + + private OptionalValue streamOptions; + + public static final String JSON_PROPERTY_CALL_HEADERS = "callHeaders"; + + private OptionalValue> callHeaders; + + public StreamDetailsImpl() {} + + protected StreamDetailsImpl( + OptionalValue endpoint, + OptionalValue streamOptions, + OptionalValue> callHeaders) { + this.endpoint = endpoint; + this.streamOptions = streamOptions; + this.callHeaders = callHeaders; + } + + @JsonIgnore + public String getEndpoint() { + return endpoint.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ENDPOINT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue endpoint() { + return endpoint; + } + + @JsonIgnore + public StreamOptions getStreamOptions() { + return streamOptions.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_STREAM_OPTIONS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue streamOptions() { + return streamOptions; + } + + @JsonIgnore + public List getCallHeaders() { + return callHeaders.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_HEADERS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> callHeaders() { + return callHeaders; + } + + /** Return true if this StreamDetails object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StreamDetailsImpl streamDetails = (StreamDetailsImpl) o; + return Objects.equals(this.endpoint, streamDetails.endpoint) + && Objects.equals(this.streamOptions, streamDetails.streamOptions) + && Objects.equals(this.callHeaders, streamDetails.callHeaders); + } + + @Override + public int hashCode() { + return Objects.hash(endpoint, streamOptions, callHeaders); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StreamDetailsImpl {\n"); + sb.append(" endpoint: ").append(toIndentedString(endpoint)).append("\n"); + sb.append(" streamOptions: ").append(toIndentedString(streamOptions)).append("\n"); + sb.append(" callHeaders: ").append(toIndentedString(callHeaders)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements StreamDetails.Builder { + OptionalValue endpoint = OptionalValue.empty(); + OptionalValue streamOptions = OptionalValue.empty(); + OptionalValue> callHeaders = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_ENDPOINT, required = true) + public Builder setEndpoint(String endpoint) { + this.endpoint = OptionalValue.of(endpoint); + return this; + } + + @JsonProperty(JSON_PROPERTY_STREAM_OPTIONS) + public Builder setStreamOptions(StreamOptions streamOptions) { + this.streamOptions = OptionalValue.of(streamOptions); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_HEADERS) + public Builder setCallHeaders(List callHeaders) { + this.callHeaders = OptionalValue.of(callHeaders); + return this; + } + + public StreamDetails build() { + return new StreamDetailsImpl(endpoint, streamOptions, callHeaders); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamImpl.java new file mode 100644 index 000000000..f4cc688a8 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamImpl.java @@ -0,0 +1,117 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({StreamImpl.JSON_PROPERTY_TYPE, StreamImpl.JSON_PROPERTY_STREAM}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StreamImpl implements Stream, CallDestination { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_STREAM = "stream"; + + private OptionalValue stream; + + public StreamImpl() {} + + protected StreamImpl(OptionalValue type, OptionalValue stream) { + this.type = type; + this.stream = stream; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public StreamDetails getStream() { + return stream.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_STREAM) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue stream() { + return stream; + } + + /** Return true if this Stream object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StreamImpl stream = (StreamImpl) o; + return Objects.equals(this.type, stream.type) && Objects.equals(this.stream, stream.stream); + } + + @Override + public int hashCode() { + return Objects.hash(type, stream); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StreamImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" stream: ").append(toIndentedString(stream)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements Stream.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.STREAM); + OptionalValue stream = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.STREAM)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.STREAM, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_STREAM, required = true) + public Builder setStream(StreamDetails stream) { + this.stream = OptionalValue.of(stream); + return this; + } + + public Stream build() { + return new StreamImpl(type, stream); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptions.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptions.java new file mode 100644 index 000000000..8ccf18040 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptions.java @@ -0,0 +1,148 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; + +@JsonDeserialize(builder = StreamOptionsImpl.Builder.class) +public interface StreamOptions { + + /** Gets or Sets codec */ + public class CodecEnum extends EnumDynamic { + public static final CodecEnum PCM = new CodecEnum("PCM"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CodecEnum.class, CodecEnum::new, Arrays.asList(PCM)); + + private CodecEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CodecEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CodecEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** Gets or Sets sampleRate */ + public class SampleRateEnum extends EnumDynamic { + public static final SampleRateEnum NUMBER_8000 = new SampleRateEnum(8000); + + public static final SampleRateEnum NUMBER_16000 = new SampleRateEnum(16000); + + public static final SampleRateEnum NUMBER_24000 = new SampleRateEnum(24000); + + public static final SampleRateEnum NUMBER_44100 = new SampleRateEnum(44100); + + public static final SampleRateEnum NUMBER_48000 = new SampleRateEnum(48000); + + public static final SampleRateEnum NUMBER_96000 = new SampleRateEnum(96000); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + SampleRateEnum.class, + SampleRateEnum::new, + Arrays.asList( + NUMBER_8000, NUMBER_16000, NUMBER_24000, NUMBER_44100, NUMBER_48000, NUMBER_96000)); + + private SampleRateEnum(Integer value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static SampleRateEnum from(Integer value) { + return ENUM_SUPPORT.from(value); + } + + public static Integer valueOf(SampleRateEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Defines the version of the stream protocol. + * + * @return version + */ + Integer getVersion(); + + /** + * Defines the audio codec/format used for the stream audio payload. + * + *

Currently, only PCM is supported (uncompressed raw audio). Use sampleRate + * to configure the sampling rate for the stream. + * + * @return codec + */ + CodecEnum getCodec(); + + /** + * Defines the audio sampling rate (Hz) used for the stream. + * + *

For calls that traverse the PSTN, audio is typically sampled at 8 kHz, so using a higher + * value will not improve perceived quality. Higher sample rates can be useful for non-PSTN + * scenarios (for example, SIP/streaming paths), but will increase bandwidth usage and processing + * load. + * + * @return sampleRate + */ + SampleRateEnum getSampleRate(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StreamOptionsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param version see getter + * @return Current builder + * @see #getVersion + */ + Builder setVersion(Integer version); + + /** + * see getter + * + * @param codec see getter + * @return Current builder + * @see #getCodec + */ + Builder setCodec(CodecEnum codec); + + /** + * see getter + * + * @param sampleRate see getter + * @return Current builder + * @see #getSampleRate + */ + Builder setSampleRate(SampleRateEnum sampleRate); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + StreamOptions build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptionsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptionsImpl.java new file mode 100644 index 000000000..ad02f01bc --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptionsImpl.java @@ -0,0 +1,147 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + StreamOptionsImpl.JSON_PROPERTY_VERSION, + StreamOptionsImpl.JSON_PROPERTY_CODEC, + StreamOptionsImpl.JSON_PROPERTY_SAMPLE_RATE +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StreamOptionsImpl implements StreamOptions { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_VERSION = "version"; + + private OptionalValue version; + + public static final String JSON_PROPERTY_CODEC = "codec"; + + private OptionalValue codec; + + public static final String JSON_PROPERTY_SAMPLE_RATE = "sampleRate"; + + private OptionalValue sampleRate; + + public StreamOptionsImpl() {} + + protected StreamOptionsImpl( + OptionalValue version, + OptionalValue codec, + OptionalValue sampleRate) { + this.version = version; + this.codec = codec; + this.sampleRate = sampleRate; + } + + @JsonIgnore + public Integer getVersion() { + return version.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_VERSION) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue version() { + return version; + } + + @JsonIgnore + public CodecEnum getCodec() { + return codec.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CODEC) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue codec() { + return codec; + } + + @JsonIgnore + public SampleRateEnum getSampleRate() { + return sampleRate.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SAMPLE_RATE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue sampleRate() { + return sampleRate; + } + + /** Return true if this StreamOptions object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StreamOptionsImpl streamOptions = (StreamOptionsImpl) o; + return Objects.equals(this.version, streamOptions.version) + && Objects.equals(this.codec, streamOptions.codec) + && Objects.equals(this.sampleRate, streamOptions.sampleRate); + } + + @Override + public int hashCode() { + return Objects.hash(version, codec, sampleRate); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StreamOptionsImpl {\n"); + sb.append(" version: ").append(toIndentedString(version)).append("\n"); + sb.append(" codec: ").append(toIndentedString(codec)).append("\n"); + sb.append(" sampleRate: ").append(toIndentedString(sampleRate)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements StreamOptions.Builder { + OptionalValue version = OptionalValue.empty(); + OptionalValue codec = OptionalValue.empty(); + OptionalValue sampleRate = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_VERSION) + public Builder setVersion(Integer version) { + this.version = OptionalValue.of(version); + return this; + } + + @JsonProperty(JSON_PROPERTY_CODEC) + public Builder setCodec(CodecEnum codec) { + this.codec = OptionalValue.of(codec); + return this; + } + + @JsonProperty(JSON_PROPERTY_SAMPLE_RATE) + public Builder setSampleRate(SampleRateEnum sampleRate) { + this.sampleRate = OptionalValue.of(sampleRate); + return this; + } + + public StreamOptions build() { + return new StreamOptionsImpl(version, codec, sampleRate); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelay.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelay.java new file mode 100644 index 000000000..3efb98cc6 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelay.java @@ -0,0 +1,77 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; + +/** + * Routes the call through the Voice Relay service, enabling real-time speech-to-text (STT) and + * text-to-speech (TTS) via a WebSocket connection to the application backend. + */ +@JsonDeserialize(builder = VoiceRelayImpl.Builder.class) +public interface VoiceRelay extends CallDestination { + + /** The type property. Must have the value VOICE_RELAY. */ + public class TypeEnum extends EnumDynamic { + /** The VOICE_RELAY type. */ + public static final TypeEnum VOICE_RELAY = new TypeEnum("VOICE_RELAY"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(VOICE_RELAY)); + + private TypeEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Get voiceRelay + * + *

Field is required + * + * @return voiceRelay + */ + VoiceRelayDetails getVoiceRelay(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new VoiceRelayImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param voiceRelay see getter + * @return Current builder + * @see #getVoiceRelay + */ + Builder setVoiceRelay(VoiceRelayDetails voiceRelay); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + VoiceRelay build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetails.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetails.java new file mode 100644 index 000000000..e50484406 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetails.java @@ -0,0 +1,129 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +@JsonDeserialize(builder = VoiceRelayDetailsImpl.Builder.class) +public interface VoiceRelayDetails { + + /** + * URL to the server that will accept the web-socket request + * + *

Field is required + * + * @return endpoint + */ + String getEndpoint(); + + /** + * Allow "barge-in" during text-to-speech (TTS) playback. + * + *

When true, TTS playback is interrupted as soon as inbound speech is detected, + * unless the currently playing content is marked as uninterruptible. + * + *

When false, TTS playback continues uninterrupted, but an interruption signal is + * still sent over the WebSocket so the client application can choose to stop playback manually if + * needed. + * + * @return enableInterruptions + */ + Boolean getEnableInterruptions(); + + /** + * Name of the voice to be used when synthesizing speech. + * + *

This is the default voice used, if no override voice is provided in the web-socket TTS + * message. + * + *

Supported voices include: Emma, Brian, and others. For a complete list of available voices + * and their characteristics, see the Text-to-Speech Voices documentation. + * + *

Field is required + * + * @return ttsVoice + */ + String getTtsVoice(); + + /** + * BCP-47 language tag used for speech-to-text transcription of the inbound audio. + * + *

This value determines which language model is used for transcription. + * + *

Field is required + * + * @return sttLanguage + */ + String getSttLanguage(); + + /** + * Custom headers to be sent in the call setup. + * + * @return callHeaders + */ + List getCallHeaders(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new VoiceRelayDetailsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param endpoint see getter + * @return Current builder + * @see #getEndpoint + */ + Builder setEndpoint(String endpoint); + + /** + * see getter + * + * @param enableInterruptions see getter + * @return Current builder + * @see #getEnableInterruptions + */ + Builder setEnableInterruptions(Boolean enableInterruptions); + + /** + * see getter + * + * @param ttsVoice see getter + * @return Current builder + * @see #getTtsVoice + */ + Builder setTtsVoice(String ttsVoice); + + /** + * see getter + * + * @param sttLanguage see getter + * @return Current builder + * @see #getSttLanguage + */ + Builder setSttLanguage(String sttLanguage); + + /** + * see getter + * + * @param callHeaders see getter + * @return Current builder + * @see #getCallHeaders + */ + Builder setCallHeaders(List callHeaders); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + VoiceRelayDetails build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetailsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetailsImpl.java new file mode 100644 index 000000000..33ccd244d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetailsImpl.java @@ -0,0 +1,205 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + VoiceRelayDetailsImpl.JSON_PROPERTY_ENDPOINT, + VoiceRelayDetailsImpl.JSON_PROPERTY_ENABLE_INTERRUPTIONS, + VoiceRelayDetailsImpl.JSON_PROPERTY_TTS_VOICE, + VoiceRelayDetailsImpl.JSON_PROPERTY_STT_LANGUAGE, + VoiceRelayDetailsImpl.JSON_PROPERTY_CALL_HEADERS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class VoiceRelayDetailsImpl implements VoiceRelayDetails { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ENDPOINT = "endpoint"; + + private OptionalValue endpoint; + + public static final String JSON_PROPERTY_ENABLE_INTERRUPTIONS = "enableInterruptions"; + + private OptionalValue enableInterruptions; + + public static final String JSON_PROPERTY_TTS_VOICE = "ttsVoice"; + + private OptionalValue ttsVoice; + + public static final String JSON_PROPERTY_STT_LANGUAGE = "sttLanguage"; + + private OptionalValue sttLanguage; + + public static final String JSON_PROPERTY_CALL_HEADERS = "callHeaders"; + + private OptionalValue> callHeaders; + + public VoiceRelayDetailsImpl() {} + + protected VoiceRelayDetailsImpl( + OptionalValue endpoint, + OptionalValue enableInterruptions, + OptionalValue ttsVoice, + OptionalValue sttLanguage, + OptionalValue> callHeaders) { + this.endpoint = endpoint; + this.enableInterruptions = enableInterruptions; + this.ttsVoice = ttsVoice; + this.sttLanguage = sttLanguage; + this.callHeaders = callHeaders; + } + + @JsonIgnore + public String getEndpoint() { + return endpoint.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ENDPOINT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue endpoint() { + return endpoint; + } + + @JsonIgnore + public Boolean getEnableInterruptions() { + return enableInterruptions.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ENABLE_INTERRUPTIONS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue enableInterruptions() { + return enableInterruptions; + } + + @JsonIgnore + public String getTtsVoice() { + return ttsVoice.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TTS_VOICE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue ttsVoice() { + return ttsVoice; + } + + @JsonIgnore + public String getSttLanguage() { + return sttLanguage.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_STT_LANGUAGE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue sttLanguage() { + return sttLanguage; + } + + @JsonIgnore + public List getCallHeaders() { + return callHeaders.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_HEADERS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> callHeaders() { + return callHeaders; + } + + /** Return true if this VoiceRelayDetails object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VoiceRelayDetailsImpl voiceRelayDetails = (VoiceRelayDetailsImpl) o; + return Objects.equals(this.endpoint, voiceRelayDetails.endpoint) + && Objects.equals(this.enableInterruptions, voiceRelayDetails.enableInterruptions) + && Objects.equals(this.ttsVoice, voiceRelayDetails.ttsVoice) + && Objects.equals(this.sttLanguage, voiceRelayDetails.sttLanguage) + && Objects.equals(this.callHeaders, voiceRelayDetails.callHeaders); + } + + @Override + public int hashCode() { + return Objects.hash(endpoint, enableInterruptions, ttsVoice, sttLanguage, callHeaders); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class VoiceRelayDetailsImpl {\n"); + sb.append(" endpoint: ").append(toIndentedString(endpoint)).append("\n"); + sb.append(" enableInterruptions: ") + .append(toIndentedString(enableInterruptions)) + .append("\n"); + sb.append(" ttsVoice: ").append(toIndentedString(ttsVoice)).append("\n"); + sb.append(" sttLanguage: ").append(toIndentedString(sttLanguage)).append("\n"); + sb.append(" callHeaders: ").append(toIndentedString(callHeaders)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements VoiceRelayDetails.Builder { + OptionalValue endpoint = OptionalValue.empty(); + OptionalValue enableInterruptions = OptionalValue.empty(); + OptionalValue ttsVoice = OptionalValue.empty(); + OptionalValue sttLanguage = OptionalValue.empty(); + OptionalValue> callHeaders = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_ENDPOINT, required = true) + public Builder setEndpoint(String endpoint) { + this.endpoint = OptionalValue.of(endpoint); + return this; + } + + @JsonProperty(JSON_PROPERTY_ENABLE_INTERRUPTIONS) + public Builder setEnableInterruptions(Boolean enableInterruptions) { + this.enableInterruptions = OptionalValue.of(enableInterruptions); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_TTS_VOICE, required = true) + public Builder setTtsVoice(String ttsVoice) { + this.ttsVoice = OptionalValue.of(ttsVoice); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_STT_LANGUAGE, required = true) + public Builder setSttLanguage(String sttLanguage) { + this.sttLanguage = OptionalValue.of(sttLanguage); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_HEADERS) + public Builder setCallHeaders(List callHeaders) { + this.callHeaders = OptionalValue.of(callHeaders); + return this; + } + + public VoiceRelayDetails build() { + return new VoiceRelayDetailsImpl( + endpoint, enableInterruptions, ttsVoice, sttLanguage, callHeaders); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayImpl.java new file mode 100644 index 000000000..909dfd78d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayImpl.java @@ -0,0 +1,119 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({VoiceRelayImpl.JSON_PROPERTY_TYPE, VoiceRelayImpl.JSON_PROPERTY_VOICE_RELAY}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class VoiceRelayImpl implements VoiceRelay, CallDestination { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_VOICE_RELAY = "voiceRelay"; + + private OptionalValue voiceRelay; + + public VoiceRelayImpl() {} + + protected VoiceRelayImpl( + OptionalValue type, OptionalValue voiceRelay) { + this.type = type; + this.voiceRelay = voiceRelay; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public VoiceRelayDetails getVoiceRelay() { + return voiceRelay.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_VOICE_RELAY) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue voiceRelay() { + return voiceRelay; + } + + /** Return true if this VoiceRelay object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VoiceRelayImpl voiceRelay = (VoiceRelayImpl) o; + return Objects.equals(this.type, voiceRelay.type) + && Objects.equals(this.voiceRelay, voiceRelay.voiceRelay); + } + + @Override + public int hashCode() { + return Objects.hash(type, voiceRelay); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class VoiceRelayImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" voiceRelay: ").append(toIndentedString(voiceRelay)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements VoiceRelay.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.VOICE_RELAY); + OptionalValue voiceRelay = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.VOICE_RELAY)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.VOICE_RELAY, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_VOICE_RELAY, required = true) + public Builder setVoiceRelay(VoiceRelayDetails voiceRelay) { + this.voiceRelay = OptionalValue.of(voiceRelay); + return this; + } + + public VoiceRelay build() { + return new VoiceRelayImpl(type, voiceRelay); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallDestinationInternal.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallDestinationInternal.java new file mode 100644 index 000000000..bb02c8b66 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallDestinationInternal.java @@ -0,0 +1,6 @@ +package com.sinch.sdk.domains.voice.models.v2.destination.internal; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(using = CallDestinationInternalImpl.CallDestinationInternalImplDeserializer.class) +public interface CallDestinationInternal {} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallDestinationInternalImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallDestinationInternalImpl.java new file mode 100644 index 000000000..2f6b2c989 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallDestinationInternalImpl.java @@ -0,0 +1,182 @@ +package com.sinch.sdk.domains.voice.models.v2.destination.internal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.sinch.sdk.core.models.AbstractOpenApiSchema; +import com.sinch.sdk.core.utils.databind.JSONNavigator; +import com.sinch.sdk.domains.voice.models.v2.destination.PhoneImpl; +import com.sinch.sdk.domains.voice.models.v2.destination.SipImpl; +import com.sinch.sdk.domains.voice.models.v2.destination.StreamImpl; +import com.sinch.sdk.domains.voice.models.v2.destination.VoiceRelayImpl; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +@JsonDeserialize(using = CallDestinationInternalImpl.CallDestinationInternalImplDeserializer.class) +@JsonSerialize(using = CallDestinationInternalImpl.CallDestinationInternalImplSerializer.class) +public class CallDestinationInternalImpl extends AbstractOpenApiSchema + implements CallDestinationInternal { + private static final Logger log = Logger.getLogger(CallDestinationInternalImpl.class.getName()); + + public static final class CallDestinationInternalImplSerializer + extends StdSerializer { + private static final long serialVersionUID = 1L; + + public CallDestinationInternalImplSerializer(Class t) { + super(t); + } + + public CallDestinationInternalImplSerializer() { + this(null); + } + + @Override + public void serialize( + CallDestinationInternalImpl value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } + } + + public static final class CallDestinationInternalImplDeserializer + extends StdDeserializer { + + private static final long serialVersionUID = 1L; + + public CallDestinationInternalImplDeserializer() { + this(CallDestinationInternalImpl.class); + } + + public CallDestinationInternalImplDeserializer(Class vc) { + super(vc); + } + + @Override + public CallDestinationInternalImpl deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode tree = jp.readValueAsTree(); + JsonNode discriminator = tree.get("type"); + String discriminatorValue = null != discriminator ? discriminator.asText() : null; + Class cls = null != discriminatorValue ? mappings.get(discriminatorValue) : null; + if (null == cls) { + throw new IOException( + String.format( + "Failed deserialization for CallDestinationInternalImpl: unknown 'type' value '%s'." + + " Possible values: %s", + discriminatorValue, mappings.keySet())); + } + Object deserialized = tree.traverse(jp.getCodec()).readValueAs(cls); + CallDestinationInternalImpl ret = new CallDestinationInternalImpl(); + ret.setActualInstance(deserialized); + return ret; + } + + /** Handle deserialization of the 'null' value. */ + @Override + public CallDestinationInternalImpl getNullValue(DeserializationContext ctxt) + throws JsonMappingException { + throw new JsonMappingException( + ctxt.getParser(), "CallDestinationInternalImpl cannot be null"); + } + } + + // store a list of schema names defined in oneOf + public static final Map> schemas = new HashMap<>(); + + // discriminator value -> class + private static final Map> mappings = new HashMap<>(); + + public CallDestinationInternalImpl() { + super("oneOf", Boolean.FALSE); + } + + static { + schemas.put("PhoneImpl", PhoneImpl.class); + schemas.put("SipImpl", SipImpl.class); + schemas.put("StreamImpl", StreamImpl.class); + schemas.put("VoiceRelayImpl", VoiceRelayImpl.class); + JSONNavigator.registerDescendants( + CallDestinationInternalImpl.class, Collections.unmodifiableMap(schemas)); + // Initialize and register the discriminator mappings. + mappings.put("PHONE", PhoneImpl.class); + mappings.put("SIP", SipImpl.class); + mappings.put("STREAM", StreamImpl.class); + mappings.put("VOICE_RELAY", VoiceRelayImpl.class); + JSONNavigator.registerDiscriminator(CallDestinationInternalImpl.class, "type", mappings); + } + + @Override + public Map> getSchemas() { + return CallDestinationInternalImpl.schemas; + } + + /** + * Set the instance that matches the oneOf child schema, check the instance parameter is valid + * against the oneOf child schemas: PhoneImpl, SipImpl, StreamImpl, VoiceRelayImpl + */ + @Override + public void setActualInstance(Object instance) { + for (Class schema : schemas.values()) { + if (JSONNavigator.isInstanceOf(schema, instance, new HashSet>())) { + super.setActualInstance(instance); + return; + } + } + throw new RuntimeException( + "Invalid instance type. Must be PhoneImpl, SipImpl, StreamImpl, VoiceRelayImpl"); + } + + /** + * Get the actual instance, which can be the following: PhoneImpl, SipImpl, StreamImpl, + * VoiceRelayImpl + * + * @return The actual instance + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + public static class Deserializer + extends StdDeserializer { + + public Deserializer() { + this(null); + } + + public Deserializer( + Class vc) { + super(vc); + } + + @Override + public com.sinch.sdk.domains.voice.models.v2.destination.CallDestination deserialize( + JsonParser jp, DeserializationContext ctxt) throws IOException { + + Object deserialized = jp.readValueAs(CallDestinationInternalImpl.class).getActualInstance(); + if (null == deserialized) { + return null; + } + if (!(deserialized + instanceof com.sinch.sdk.domains.voice.models.v2.destination.CallDestination)) { + log.log(Level.SEVERE, "Input data does not match schema ", deserialized); + return null; + } + + return (com.sinch.sdk.domains.voice.models.v2.destination.CallDestination) deserialized; + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallOriginInternal.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallOriginInternal.java new file mode 100644 index 000000000..75a1019b9 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallOriginInternal.java @@ -0,0 +1,6 @@ +package com.sinch.sdk.domains.voice.models.v2.destination.internal; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(using = CallOriginInternalImpl.CallOriginInternalImplDeserializer.class) +public interface CallOriginInternal {} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallOriginInternalImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallOriginInternalImpl.java new file mode 100644 index 000000000..01cf8cf58 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/destination/internal/CallOriginInternalImpl.java @@ -0,0 +1,170 @@ +package com.sinch.sdk.domains.voice.models.v2.destination.internal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.sinch.sdk.core.models.AbstractOpenApiSchema; +import com.sinch.sdk.core.utils.databind.JSONNavigator; +import com.sinch.sdk.domains.voice.models.v2.destination.PhoneImpl; +import com.sinch.sdk.domains.voice.models.v2.destination.SipFromImpl; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +@JsonDeserialize(using = CallOriginInternalImpl.CallOriginInternalImplDeserializer.class) +@JsonSerialize(using = CallOriginInternalImpl.CallOriginInternalImplSerializer.class) +public class CallOriginInternalImpl extends AbstractOpenApiSchema implements CallOriginInternal { + private static final Logger log = Logger.getLogger(CallOriginInternalImpl.class.getName()); + + public static final class CallOriginInternalImplSerializer + extends StdSerializer { + private static final long serialVersionUID = 1L; + + public CallOriginInternalImplSerializer(Class t) { + super(t); + } + + public CallOriginInternalImplSerializer() { + this(null); + } + + @Override + public void serialize( + CallOriginInternalImpl value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } + } + + public static final class CallOriginInternalImplDeserializer + extends StdDeserializer { + + private static final long serialVersionUID = 1L; + + public CallOriginInternalImplDeserializer() { + this(CallOriginInternalImpl.class); + } + + public CallOriginInternalImplDeserializer(Class vc) { + super(vc); + } + + @Override + public CallOriginInternalImpl deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode tree = jp.readValueAsTree(); + JsonNode discriminator = tree.get("type"); + String discriminatorValue = null != discriminator ? discriminator.asText() : null; + Class cls = null != discriminatorValue ? mappings.get(discriminatorValue) : null; + if (null == cls) { + throw new IOException( + String.format( + "Failed deserialization for CallOriginInternalImpl: unknown 'type' value '%s'." + + " Possible values: %s", + discriminatorValue, mappings.keySet())); + } + Object deserialized = tree.traverse(jp.getCodec()).readValueAs(cls); + CallOriginInternalImpl ret = new CallOriginInternalImpl(); + ret.setActualInstance(deserialized); + return ret; + } + + /** Handle deserialization of the 'null' value. */ + @Override + public CallOriginInternalImpl getNullValue(DeserializationContext ctxt) + throws JsonMappingException { + throw new JsonMappingException(ctxt.getParser(), "CallOriginInternalImpl cannot be null"); + } + } + + // store a list of schema names defined in oneOf + public static final Map> schemas = new HashMap<>(); + + // discriminator value -> class + private static final Map> mappings = new HashMap<>(); + + public CallOriginInternalImpl() { + super("oneOf", Boolean.FALSE); + } + + static { + schemas.put("PhoneImpl", PhoneImpl.class); + schemas.put("SipFromImpl", SipFromImpl.class); + JSONNavigator.registerDescendants( + CallOriginInternalImpl.class, Collections.unmodifiableMap(schemas)); + // Initialize and register the discriminator mappings. + mappings.put("PHONE", PhoneImpl.class); + mappings.put("SIP", SipFromImpl.class); + JSONNavigator.registerDiscriminator(CallOriginInternalImpl.class, "type", mappings); + } + + @Override + public Map> getSchemas() { + return CallOriginInternalImpl.schemas; + } + + /** + * Set the instance that matches the oneOf child schema, check the instance parameter is valid + * against the oneOf child schemas: PhoneImpl, SipFromImpl + */ + @Override + public void setActualInstance(Object instance) { + for (Class schema : schemas.values()) { + if (JSONNavigator.isInstanceOf(schema, instance, new HashSet>())) { + super.setActualInstance(instance); + return; + } + } + throw new RuntimeException("Invalid instance type. Must be PhoneImpl, SipFromImpl"); + } + + /** + * Get the actual instance, which can be the following: PhoneImpl, SipFromImpl + * + * @return The actual instance + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + public static class Deserializer + extends StdDeserializer { + + public Deserializer() { + this(null); + } + + public Deserializer(Class vc) { + super(vc); + } + + @Override + public com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin deserialize( + JsonParser jp, DeserializationContext ctxt) throws IOException { + + Object deserialized = jp.readValueAs(CallOriginInternalImpl.class).getActualInstance(); + if (null == deserialized) { + return null; + } + if (!(deserialized instanceof com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin)) { + log.log(Level.SEVERE, "Input data does not match schema ", deserialized); + return null; + } + + return (com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin) deserialized; + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommand.java new file mode 100644 index 000000000..df3151fb7 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommand.java @@ -0,0 +1,81 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * AMD (Answering Machine Detection) command to detect what answered the call. Possible outcomes + * are: human, machine, beep, or unknown. + * + *

This is a non-blocking command — the next command in the sequence executes immediately while + * detection runs in parallel. Results are delivered via the events property. + */ +@JsonDeserialize(builder = AmdCommandImpl.Builder.class) +public interface AmdCommand extends SvamlCommand { + + /** The command property. Must have the value amd. */ + public class CommandEnum extends EnumDynamic { + /** The amd command. */ + public static final CommandEnum AMD = new CommandEnum("amd"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(AMD)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * SVAML commands to execute based on the answering machine detection result. These events define + * different call flows depending on whether a human, machine, beep, or unknown entity answers the + * call. + * + * @return events + */ + AmdEvents getEvents(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new AmdCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param events see getter + * @return Current builder + * @see #getEvents + */ + Builder setEvents(AmdEvents events); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + AmdCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommandImpl.java new file mode 100644 index 000000000..0db35c295 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommandImpl.java @@ -0,0 +1,118 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({AmdCommandImpl.JSON_PROPERTY_COMMAND, AmdCommandImpl.JSON_PROPERTY_EVENTS}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class AmdCommandImpl implements AmdCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_EVENTS = "events"; + + private OptionalValue events; + + public AmdCommandImpl() {} + + protected AmdCommandImpl(OptionalValue command, OptionalValue events) { + this.command = command; + this.events = events; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public AmdEvents getEvents() { + return events.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue events() { + return events; + } + + /** Return true if this AmdCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AmdCommandImpl amdCommand = (AmdCommandImpl) o; + return Objects.equals(this.command, amdCommand.command) + && Objects.equals(this.events, amdCommand.events); + } + + @Override + public int hashCode() { + return Objects.hash(command, events); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class AmdCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" events: ").append(toIndentedString(events)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements AmdCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.AMD); + OptionalValue events = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.AMD)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.AMD, command)); + } + return this; + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + public Builder setEvents(AmdEvents events) { + this.events = OptionalValue.of(events); + return this; + } + + public AmdCommand build() { + return new AmdCommandImpl(command, events); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEvents.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEvents.java new file mode 100644 index 000000000..58f798b77 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEvents.java @@ -0,0 +1,97 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** + * SVAML commands to execute based on the answering machine detection result. These events define + * different call flows depending on whether a human, machine, beep, or unknown entity answers the + * call. + */ +@JsonDeserialize(builder = AmdEventsImpl.Builder.class) +public interface AmdEvents { + + /** + * SVAML commands to be executed when a human is detected + * + * @return onHuman + */ + List getOnHuman(); + + /** + * SVAML commands to be executed when a machine is detected + * + * @return onMachine + */ + List getOnMachine(); + + /** + * SVAML commands to be executed when a beep is detected + * + * @return onBeep + */ + List getOnBeep(); + + /** + * SVAML commands to be executed when an unknown event is detected + * + * @return onUnknown + */ + List getOnUnknown(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new AmdEventsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param onHuman see getter + * @return Current builder + * @see #getOnHuman + */ + Builder setOnHuman(List onHuman); + + /** + * see getter + * + * @param onMachine see getter + * @return Current builder + * @see #getOnMachine + */ + Builder setOnMachine(List onMachine); + + /** + * see getter + * + * @param onBeep see getter + * @return Current builder + * @see #getOnBeep + */ + Builder setOnBeep(List onBeep); + + /** + * see getter + * + * @param onUnknown see getter + * @return Current builder + * @see #getOnUnknown + */ + Builder setOnUnknown(List onUnknown); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + AmdEvents build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEventsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEventsImpl.java new file mode 100644 index 000000000..d694a94ee --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEventsImpl.java @@ -0,0 +1,175 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + AmdEventsImpl.JSON_PROPERTY_ON_HUMAN, + AmdEventsImpl.JSON_PROPERTY_ON_MACHINE, + AmdEventsImpl.JSON_PROPERTY_ON_BEEP, + AmdEventsImpl.JSON_PROPERTY_ON_UNKNOWN +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class AmdEventsImpl implements AmdEvents { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ON_HUMAN = "onHuman"; + + private OptionalValue> onHuman; + + public static final String JSON_PROPERTY_ON_MACHINE = "onMachine"; + + private OptionalValue> onMachine; + + public static final String JSON_PROPERTY_ON_BEEP = "onBeep"; + + private OptionalValue> onBeep; + + public static final String JSON_PROPERTY_ON_UNKNOWN = "onUnknown"; + + private OptionalValue> onUnknown; + + public AmdEventsImpl() {} + + protected AmdEventsImpl( + OptionalValue> onHuman, + OptionalValue> onMachine, + OptionalValue> onBeep, + OptionalValue> onUnknown) { + this.onHuman = onHuman; + this.onMachine = onMachine; + this.onBeep = onBeep; + this.onUnknown = onUnknown; + } + + @JsonIgnore + public List getOnHuman() { + return onHuman.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_HUMAN) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onHuman() { + return onHuman; + } + + @JsonIgnore + public List getOnMachine() { + return onMachine.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_MACHINE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onMachine() { + return onMachine; + } + + @JsonIgnore + public List getOnBeep() { + return onBeep.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_BEEP) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onBeep() { + return onBeep; + } + + @JsonIgnore + public List getOnUnknown() { + return onUnknown.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_UNKNOWN) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onUnknown() { + return onUnknown; + } + + /** Return true if this AmdEvents object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AmdEventsImpl amdEvents = (AmdEventsImpl) o; + return Objects.equals(this.onHuman, amdEvents.onHuman) + && Objects.equals(this.onMachine, amdEvents.onMachine) + && Objects.equals(this.onBeep, amdEvents.onBeep) + && Objects.equals(this.onUnknown, amdEvents.onUnknown); + } + + @Override + public int hashCode() { + return Objects.hash(onHuman, onMachine, onBeep, onUnknown); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class AmdEventsImpl {\n"); + sb.append(" onHuman: ").append(toIndentedString(onHuman)).append("\n"); + sb.append(" onMachine: ").append(toIndentedString(onMachine)).append("\n"); + sb.append(" onBeep: ").append(toIndentedString(onBeep)).append("\n"); + sb.append(" onUnknown: ").append(toIndentedString(onUnknown)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements AmdEvents.Builder { + OptionalValue> onHuman = OptionalValue.empty(); + OptionalValue> onMachine = OptionalValue.empty(); + OptionalValue> onBeep = OptionalValue.empty(); + OptionalValue> onUnknown = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_ON_HUMAN) + public Builder setOnHuman(List onHuman) { + this.onHuman = OptionalValue.of(onHuman); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_MACHINE) + public Builder setOnMachine(List onMachine) { + this.onMachine = OptionalValue.of(onMachine); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_BEEP) + public Builder setOnBeep(List onBeep) { + this.onBeep = OptionalValue.of(onBeep); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_UNKNOWN) + public Builder setOnUnknown(List onUnknown) { + this.onUnknown = OptionalValue.of(onUnknown); + return this; + } + + public AmdEvents build() { + return new AmdEventsImpl(onHuman, onMachine, onBeep, onUnknown); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommand.java new file mode 100644 index 000000000..42853b5db --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommand.java @@ -0,0 +1,60 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Answers an inbound call leg. This is a non-blocking command — execution continues to the next + * command in the sequence immediately after the answer is initiated. + */ +@JsonDeserialize(builder = AnswerCommandImpl.Builder.class) +public interface AnswerCommand extends SvamlCommand { + + /** The command property. Must have the value answer. */ + public class CommandEnum extends EnumDynamic { + /** The answer command. */ + public static final CommandEnum ANSWER = new CommandEnum("answer"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(ANSWER)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new AnswerCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * Create instance + * + * @return The instance build with current builder values + */ + AnswerCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommandImpl.java new file mode 100644 index 000000000..550b63014 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommandImpl.java @@ -0,0 +1,93 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({AnswerCommandImpl.JSON_PROPERTY_COMMAND}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class AnswerCommandImpl implements AnswerCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public AnswerCommandImpl() {} + + protected AnswerCommandImpl(OptionalValue command) { + this.command = command; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + /** Return true if this AnswerCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AnswerCommandImpl answerCommand = (AnswerCommandImpl) o; + return Objects.equals(this.command, answerCommand.command); + } + + @Override + public int hashCode() { + return Objects.hash(command); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class AnswerCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements AnswerCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.ANSWER); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.ANSWER)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.ANSWER, command)); + } + return this; + } + + public AnswerCommand build() { + return new AnswerCommandImpl(command); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommand.java new file mode 100644 index 000000000..b186c2120 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommand.java @@ -0,0 +1,83 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Adds the current call to a bridge, enabling bidirectional audio communication with other calls in + * the same session. This is a non-blocking command — execution continues to the next command in the + * sequence immediately after the call joins the bridge. + * + *

Bridges are created automatically when referenced by name. If a bridge with the specified name + * already exists, the call joins that bridge; otherwise, a new bridge is created. + */ +@JsonDeserialize(builder = BridgeCallCommandImpl.Builder.class) +public interface BridgeCallCommand extends SvamlCommand { + + /** The command property. Must have the value bridgeCall. */ + public class CommandEnum extends EnumDynamic { + /** The bridgeCall command. */ + public static final CommandEnum BRIDGE_CALL = new CommandEnum("bridgeCall"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(BRIDGE_CALL)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Name of the bridge to join. If no bridge with this name exists in the session, a new one is + * created automatically. + * + *

Field is required + * + * @return bridgeName + */ + String getBridgeName(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new BridgeCallCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param bridgeName see getter + * @return Current builder + * @see #getBridgeName + */ + Builder setBridgeName(String bridgeName); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + BridgeCallCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommandImpl.java new file mode 100644 index 000000000..b83b43b8b --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommandImpl.java @@ -0,0 +1,122 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + BridgeCallCommandImpl.JSON_PROPERTY_COMMAND, + BridgeCallCommandImpl.JSON_PROPERTY_BRIDGE_NAME +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class BridgeCallCommandImpl implements BridgeCallCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_BRIDGE_NAME = "bridgeName"; + + private OptionalValue bridgeName; + + public BridgeCallCommandImpl() {} + + protected BridgeCallCommandImpl( + OptionalValue command, OptionalValue bridgeName) { + this.command = command; + this.bridgeName = bridgeName; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getBridgeName() { + return bridgeName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_BRIDGE_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue bridgeName() { + return bridgeName; + } + + /** Return true if this BridgeCallCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BridgeCallCommandImpl bridgeCallCommand = (BridgeCallCommandImpl) o; + return Objects.equals(this.command, bridgeCallCommand.command) + && Objects.equals(this.bridgeName, bridgeCallCommand.bridgeName); + } + + @Override + public int hashCode() { + return Objects.hash(command, bridgeName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class BridgeCallCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" bridgeName: ").append(toIndentedString(bridgeName)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements BridgeCallCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.BRIDGE_CALL); + OptionalValue bridgeName = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.BRIDGE_CALL)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.BRIDGE_CALL, command)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_BRIDGE_NAME, required = true) + public Builder setBridgeName(String bridgeName) { + this.bridgeName = OptionalValue.of(bridgeName); + return this; + } + + public BridgeCallCommand build() { + return new BridgeCallCommandImpl(command, bridgeName); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CallEvents.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CallEvents.java new file mode 100644 index 000000000..ebd777c10 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CallEvents.java @@ -0,0 +1,125 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** Webhook to handle call events, used when callBehaviors are set to WEBHOOK */ +@JsonDeserialize(builder = CallEventsImpl.Builder.class) +public interface CallEvents { + + /** + * SVAML commands to be executed when the call is answered + * + * @return onAnswer + */ + List getOnAnswer(); + + /** + * SVAML commands to be executed when the call is busy + * + * @return onBusy + */ + List getOnBusy(); + + /** + * SVAML commands to be executed when the call is rejected + * + * @return onReject + */ + List getOnReject(); + + /** + * SVAML commands to be executed when the call is timed out + * + * @return onTimeout + */ + List getOnTimeout(); + + /** + * SVAML commands to be executed when the call is hung up + * + * @return onHangup + */ + List getOnHangup(); + + /** + * SVAML commands to be executed when the call fails + * + * @return onFailure + */ + List getOnFailure(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new CallEventsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param onAnswer see getter + * @return Current builder + * @see #getOnAnswer + */ + Builder setOnAnswer(List onAnswer); + + /** + * see getter + * + * @param onBusy see getter + * @return Current builder + * @see #getOnBusy + */ + Builder setOnBusy(List onBusy); + + /** + * see getter + * + * @param onReject see getter + * @return Current builder + * @see #getOnReject + */ + Builder setOnReject(List onReject); + + /** + * see getter + * + * @param onTimeout see getter + * @return Current builder + * @see #getOnTimeout + */ + Builder setOnTimeout(List onTimeout); + + /** + * see getter + * + * @param onHangup see getter + * @return Current builder + * @see #getOnHangup + */ + Builder setOnHangup(List onHangup); + + /** + * see getter + * + * @param onFailure see getter + * @return Current builder + * @see #getOnFailure + */ + Builder setOnFailure(List onFailure); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + CallEvents build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CallEventsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CallEventsImpl.java new file mode 100644 index 000000000..03d875f3c --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CallEventsImpl.java @@ -0,0 +1,229 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + CallEventsImpl.JSON_PROPERTY_ON_ANSWER, + CallEventsImpl.JSON_PROPERTY_ON_BUSY, + CallEventsImpl.JSON_PROPERTY_ON_REJECT, + CallEventsImpl.JSON_PROPERTY_ON_TIMEOUT, + CallEventsImpl.JSON_PROPERTY_ON_HANGUP, + CallEventsImpl.JSON_PROPERTY_ON_FAILURE +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class CallEventsImpl implements CallEvents { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ON_ANSWER = "onAnswer"; + + private OptionalValue> onAnswer; + + public static final String JSON_PROPERTY_ON_BUSY = "onBusy"; + + private OptionalValue> onBusy; + + public static final String JSON_PROPERTY_ON_REJECT = "onReject"; + + private OptionalValue> onReject; + + public static final String JSON_PROPERTY_ON_TIMEOUT = "onTimeout"; + + private OptionalValue> onTimeout; + + public static final String JSON_PROPERTY_ON_HANGUP = "onHangup"; + + private OptionalValue> onHangup; + + public static final String JSON_PROPERTY_ON_FAILURE = "onFailure"; + + private OptionalValue> onFailure; + + public CallEventsImpl() {} + + protected CallEventsImpl( + OptionalValue> onAnswer, + OptionalValue> onBusy, + OptionalValue> onReject, + OptionalValue> onTimeout, + OptionalValue> onHangup, + OptionalValue> onFailure) { + this.onAnswer = onAnswer; + this.onBusy = onBusy; + this.onReject = onReject; + this.onTimeout = onTimeout; + this.onHangup = onHangup; + this.onFailure = onFailure; + } + + @JsonIgnore + public List getOnAnswer() { + return onAnswer.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_ANSWER) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onAnswer() { + return onAnswer; + } + + @JsonIgnore + public List getOnBusy() { + return onBusy.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_BUSY) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onBusy() { + return onBusy; + } + + @JsonIgnore + public List getOnReject() { + return onReject.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_REJECT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onReject() { + return onReject; + } + + @JsonIgnore + public List getOnTimeout() { + return onTimeout.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_TIMEOUT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onTimeout() { + return onTimeout; + } + + @JsonIgnore + public List getOnHangup() { + return onHangup.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_HANGUP) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onHangup() { + return onHangup; + } + + @JsonIgnore + public List getOnFailure() { + return onFailure.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_FAILURE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onFailure() { + return onFailure; + } + + /** Return true if this CallEvents object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CallEventsImpl callEvents = (CallEventsImpl) o; + return Objects.equals(this.onAnswer, callEvents.onAnswer) + && Objects.equals(this.onBusy, callEvents.onBusy) + && Objects.equals(this.onReject, callEvents.onReject) + && Objects.equals(this.onTimeout, callEvents.onTimeout) + && Objects.equals(this.onHangup, callEvents.onHangup) + && Objects.equals(this.onFailure, callEvents.onFailure); + } + + @Override + public int hashCode() { + return Objects.hash(onAnswer, onBusy, onReject, onTimeout, onHangup, onFailure); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CallEventsImpl {\n"); + sb.append(" onAnswer: ").append(toIndentedString(onAnswer)).append("\n"); + sb.append(" onBusy: ").append(toIndentedString(onBusy)).append("\n"); + sb.append(" onReject: ").append(toIndentedString(onReject)).append("\n"); + sb.append(" onTimeout: ").append(toIndentedString(onTimeout)).append("\n"); + sb.append(" onHangup: ").append(toIndentedString(onHangup)).append("\n"); + sb.append(" onFailure: ").append(toIndentedString(onFailure)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements CallEvents.Builder { + OptionalValue> onAnswer = OptionalValue.empty(); + OptionalValue> onBusy = OptionalValue.empty(); + OptionalValue> onReject = OptionalValue.empty(); + OptionalValue> onTimeout = OptionalValue.empty(); + OptionalValue> onHangup = OptionalValue.empty(); + OptionalValue> onFailure = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_ON_ANSWER) + public Builder setOnAnswer(List onAnswer) { + this.onAnswer = OptionalValue.of(onAnswer); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_BUSY) + public Builder setOnBusy(List onBusy) { + this.onBusy = OptionalValue.of(onBusy); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_REJECT) + public Builder setOnReject(List onReject) { + this.onReject = OptionalValue.of(onReject); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_TIMEOUT) + public Builder setOnTimeout(List onTimeout) { + this.onTimeout = OptionalValue.of(onTimeout); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_HANGUP) + public Builder setOnHangup(List onHangup) { + this.onHangup = OptionalValue.of(onHangup); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_FAILURE) + public Builder setOnFailure(List onFailure) { + this.onFailure = OptionalValue.of(onFailure); + return this; + } + + public CallEvents build() { + return new CallEventsImpl(onAnswer, onBusy, onReject, onTimeout, onHangup, onFailure); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommand.java new file mode 100644 index 000000000..400ae8403 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommand.java @@ -0,0 +1,121 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Triggers a mid-call Custom Event: sends a Sinch Event request to url and executes + * the SVAML commands returned in the response. + * + *

Sent on the wire as the webhook command. + */ +@JsonDeserialize(builder = CustomEventCommandImpl.Builder.class) +public interface CustomEventCommand extends SvamlCommand { + + /** The command property. Must have the value webhook. */ + public class CommandEnum extends EnumDynamic { + /** The webhook command. */ + public static final CommandEnum WEBHOOK = new CommandEnum("webhook"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(WEBHOOK)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Name for this Custom Event. When triggered, the Sinch Event request's event + * property will contain this name prepended with call.customEvent.. + * + *

For example, if customEventName is set to "my.custom.event", the + * event will be delivered as "call.customEvent.my.custom.event". + * + *

Field is required + * + * @return customEventName + */ + String getCustomEventName(); + + /** + * URL of the endpoint to send the mid-call Sinch Event to. + * + *

Field is required + * + * @return url + */ + String getUrl(); + + /** + * Fallback URL used when the primary URL fails. + * + *

A failed request is re-sent to this URL immediately. After repeated consecutive failures of + * the primary URL, requests are sent only here until the primary URL recovers. + * + * @return fallbackUrl + */ + String getFallbackUrl(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new CustomEventCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param customEventName see getter + * @return Current builder + * @see #getCustomEventName + */ + Builder setCustomEventName(String customEventName); + + /** + * see getter + * + * @param url see getter + * @return Current builder + * @see #getUrl + */ + Builder setUrl(String url); + + /** + * see getter + * + * @param fallbackUrl see getter + * @return Current builder + * @see #getFallbackUrl + */ + Builder setFallbackUrl(String fallbackUrl); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + CustomEventCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommandImpl.java new file mode 100644 index 000000000..15e09fa67 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommandImpl.java @@ -0,0 +1,177 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + CustomEventCommandImpl.JSON_PROPERTY_COMMAND, + CustomEventCommandImpl.JSON_PROPERTY_WEBHOOK_NAME, + CustomEventCommandImpl.JSON_PROPERTY_URL, + CustomEventCommandImpl.JSON_PROPERTY_FALLBACK_URL +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class CustomEventCommandImpl implements CustomEventCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_WEBHOOK_NAME = "webhookName"; + + private OptionalValue customEventName; + + public static final String JSON_PROPERTY_URL = "url"; + + private OptionalValue url; + + public static final String JSON_PROPERTY_FALLBACK_URL = "fallbackUrl"; + + private OptionalValue fallbackUrl; + + public CustomEventCommandImpl() {} + + protected CustomEventCommandImpl( + OptionalValue command, + OptionalValue customEventName, + OptionalValue url, + OptionalValue fallbackUrl) { + this.command = command; + this.customEventName = customEventName; + this.url = url; + this.fallbackUrl = fallbackUrl; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getCustomEventName() { + return customEventName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_WEBHOOK_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue customEventName() { + return customEventName; + } + + @JsonIgnore + public String getUrl() { + return url.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_URL) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue url() { + return url; + } + + @JsonIgnore + public String getFallbackUrl() { + return fallbackUrl.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_FALLBACK_URL) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue fallbackUrl() { + return fallbackUrl; + } + + /** Return true if this CustomEventCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CustomEventCommandImpl customEventCommand = (CustomEventCommandImpl) o; + return Objects.equals(this.command, customEventCommand.command) + && Objects.equals(this.customEventName, customEventCommand.customEventName) + && Objects.equals(this.url, customEventCommand.url) + && Objects.equals(this.fallbackUrl, customEventCommand.fallbackUrl); + } + + @Override + public int hashCode() { + return Objects.hash(command, customEventName, url, fallbackUrl); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CustomEventCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" customEventName: ").append(toIndentedString(customEventName)).append("\n"); + sb.append(" url: ").append(toIndentedString(url)).append("\n"); + sb.append(" fallbackUrl: ").append(toIndentedString(fallbackUrl)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements CustomEventCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.WEBHOOK); + OptionalValue customEventName = OptionalValue.empty(); + OptionalValue url = OptionalValue.empty(); + OptionalValue fallbackUrl = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.WEBHOOK)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.WEBHOOK, command)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_WEBHOOK_NAME, required = true) + public Builder setCustomEventName(String customEventName) { + this.customEventName = OptionalValue.of(customEventName); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_URL, required = true) + public Builder setUrl(String url) { + this.url = OptionalValue.of(url); + return this; + } + + @JsonProperty(JSON_PROPERTY_FALLBACK_URL) + public Builder setFallbackUrl(String fallbackUrl) { + this.fallbackUrl = OptionalValue.of(fallbackUrl); + return this; + } + + public CustomEventCommand build() { + return new CustomEventCommandImpl(command, customEventName, url, fallbackUrl); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommand.java new file mode 100644 index 000000000..408193008 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommand.java @@ -0,0 +1,175 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination; +import com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Initiates a new outbound call leg within the current session. + * + *

This is a non-blocking command — the next command in the sequence executes immediately while + * the call is being established in parallel. Call lifecycle events (answer, busy, reject, timeout, + * hangup, failure) are handled via the events property. + * + *

The from and to endpoint types should ideally match. If they differ, + * the platform attempts to convert the from value to be compatible with the to + * type. For example, PSTN supports only E.164 phone numbers, so a SIP address such as + * sip:46701234567@acme.se can be converted to an E.164 number. If the from + * value cannot be converted, it defaults to null (anonymous). + */ +@JsonDeserialize(builder = DialCommandImpl.Builder.class) +public interface DialCommand extends SvamlCommand { + + /** The command property. Must have the value dial. */ + public class CommandEnum extends EnumDynamic { + /** The dial command. */ + public static final CommandEnum DIAL = new CommandEnum("dial"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(DIAL)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Identifier for this call leg within the session. Must be unique across all active call legs in + * the session. + * + *

Other commands (e.g., hangup) can reference this name to target this specific + * leg. + * + * @return callName + */ + String getCallName(); + + /** + * Call origin - Phone Number or SIP endpoint + * + * @return from + */ + CallOrigin getFrom(); + + /** + * Call destination - Phone Number or Stream URI + * + *

Field is required + * + * @return to + */ + CallDestination getTo(); + + /** + * Maximum time in seconds to wait for the call to be answered. If the timeout expires without an + * answer, the onTimeout event is triggered. + * + * @return dialTimeoutDurationSeconds + */ + Integer getDialTimeoutDurationSeconds(); + + /** + * Maximum duration of the call in seconds. The call is terminated automatically when this limit + * is reached. + * + * @return maxCallDurationSeconds + */ + Integer getMaxCallDurationSeconds(); + + /** + * Webhook to handle call events, used when callBehaviors are set to WEBHOOK + * + * @return events + */ + CallEvents getEvents(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new DialCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param callName see getter + * @return Current builder + * @see #getCallName + */ + Builder setCallName(String callName); + + /** + * see getter + * + * @param from see getter + * @return Current builder + * @see #getFrom + */ + Builder setFrom(CallOrigin from); + + /** + * see getter + * + * @param to see getter + * @return Current builder + * @see #getTo + */ + Builder setTo(CallDestination to); + + /** + * see getter + * + * @param dialTimeoutDurationSeconds see getter + * @return Current builder + * @see #getDialTimeoutDurationSeconds + */ + Builder setDialTimeoutDurationSeconds(Integer dialTimeoutDurationSeconds); + + /** + * see getter + * + * @param maxCallDurationSeconds see getter + * @return Current builder + * @see #getMaxCallDurationSeconds + */ + Builder setMaxCallDurationSeconds(Integer maxCallDurationSeconds); + + /** + * see getter + * + * @param events see getter + * @return Current builder + * @see #getEvents + */ + Builder setEvents(CallEvents events); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + DialCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommandImpl.java new file mode 100644 index 000000000..84b7597a1 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommandImpl.java @@ -0,0 +1,267 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination; +import com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin; +import java.util.Objects; + +@JsonPropertyOrder({ + DialCommandImpl.JSON_PROPERTY_COMMAND, + DialCommandImpl.JSON_PROPERTY_CALL_NAME, + DialCommandImpl.JSON_PROPERTY_FROM, + DialCommandImpl.JSON_PROPERTY_TO, + DialCommandImpl.JSON_PROPERTY_DIAL_TIMEOUT_DURATION_SECONDS, + DialCommandImpl.JSON_PROPERTY_MAX_CALL_DURATION_SECONDS, + DialCommandImpl.JSON_PROPERTY_EVENTS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class DialCommandImpl implements DialCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_CALL_NAME = "callName"; + + private OptionalValue callName; + + public static final String JSON_PROPERTY_FROM = "from"; + + private OptionalValue from; + + public static final String JSON_PROPERTY_TO = "to"; + + private OptionalValue to; + + public static final String JSON_PROPERTY_DIAL_TIMEOUT_DURATION_SECONDS = + "dialTimeoutDurationSeconds"; + + private OptionalValue dialTimeoutDurationSeconds; + + public static final String JSON_PROPERTY_MAX_CALL_DURATION_SECONDS = "maxCallDurationSeconds"; + + private OptionalValue maxCallDurationSeconds; + + public static final String JSON_PROPERTY_EVENTS = "events"; + + private OptionalValue events; + + public DialCommandImpl() {} + + protected DialCommandImpl( + OptionalValue command, + OptionalValue callName, + OptionalValue from, + OptionalValue to, + OptionalValue dialTimeoutDurationSeconds, + OptionalValue maxCallDurationSeconds, + OptionalValue events) { + this.command = command; + this.callName = callName; + this.from = from; + this.to = to; + this.dialTimeoutDurationSeconds = dialTimeoutDurationSeconds; + this.maxCallDurationSeconds = maxCallDurationSeconds; + this.events = events; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getCallName() { + return callName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callName() { + return callName; + } + + @JsonIgnore + public CallOrigin getFrom() { + return from.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_FROM) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue from() { + return from; + } + + @JsonIgnore + public CallDestination getTo() { + return to.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TO) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue to() { + return to; + } + + @JsonIgnore + public Integer getDialTimeoutDurationSeconds() { + return dialTimeoutDurationSeconds.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DIAL_TIMEOUT_DURATION_SECONDS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue dialTimeoutDurationSeconds() { + return dialTimeoutDurationSeconds; + } + + @JsonIgnore + public Integer getMaxCallDurationSeconds() { + return maxCallDurationSeconds.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MAX_CALL_DURATION_SECONDS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue maxCallDurationSeconds() { + return maxCallDurationSeconds; + } + + @JsonIgnore + public CallEvents getEvents() { + return events.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue events() { + return events; + } + + /** Return true if this DialCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DialCommandImpl dialCommand = (DialCommandImpl) o; + return Objects.equals(this.command, dialCommand.command) + && Objects.equals(this.callName, dialCommand.callName) + && Objects.equals(this.from, dialCommand.from) + && Objects.equals(this.to, dialCommand.to) + && Objects.equals(this.dialTimeoutDurationSeconds, dialCommand.dialTimeoutDurationSeconds) + && Objects.equals(this.maxCallDurationSeconds, dialCommand.maxCallDurationSeconds) + && Objects.equals(this.events, dialCommand.events); + } + + @Override + public int hashCode() { + return Objects.hash( + command, callName, from, to, dialTimeoutDurationSeconds, maxCallDurationSeconds, events); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class DialCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" callName: ").append(toIndentedString(callName)).append("\n"); + sb.append(" from: ").append(toIndentedString(from)).append("\n"); + sb.append(" to: ").append(toIndentedString(to)).append("\n"); + sb.append(" dialTimeoutDurationSeconds: ") + .append(toIndentedString(dialTimeoutDurationSeconds)) + .append("\n"); + sb.append(" maxCallDurationSeconds: ") + .append(toIndentedString(maxCallDurationSeconds)) + .append("\n"); + sb.append(" events: ").append(toIndentedString(events)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements DialCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.DIAL); + OptionalValue callName = OptionalValue.empty(); + OptionalValue from = OptionalValue.empty(); + OptionalValue to = OptionalValue.empty(); + OptionalValue dialTimeoutDurationSeconds = OptionalValue.empty(); + OptionalValue maxCallDurationSeconds = OptionalValue.empty(); + OptionalValue events = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.DIAL)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.DIAL, command)); + } + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + public Builder setCallName(String callName) { + this.callName = OptionalValue.of(callName); + return this; + } + + @JsonProperty(JSON_PROPERTY_FROM) + public Builder setFrom(CallOrigin from) { + this.from = OptionalValue.of(from); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_TO, required = true) + public Builder setTo(CallDestination to) { + this.to = OptionalValue.of(to); + return this; + } + + @JsonProperty(JSON_PROPERTY_DIAL_TIMEOUT_DURATION_SECONDS) + public Builder setDialTimeoutDurationSeconds(Integer dialTimeoutDurationSeconds) { + this.dialTimeoutDurationSeconds = OptionalValue.of(dialTimeoutDurationSeconds); + return this; + } + + @JsonProperty(JSON_PROPERTY_MAX_CALL_DURATION_SECONDS) + public Builder setMaxCallDurationSeconds(Integer maxCallDurationSeconds) { + this.maxCallDurationSeconds = OptionalValue.of(maxCallDurationSeconds); + return this; + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + public Builder setEvents(CallEvents events) { + this.events = OptionalValue.of(events); + return this; + } + + public DialCommand build() { + return new DialCommandImpl( + command, callName, from, to, dialTimeoutDurationSeconds, maxCallDurationSeconds, events); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommand.java new file mode 100644 index 000000000..9ba483ad4 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommand.java @@ -0,0 +1,81 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Switches execution to another menu within the current menu context. The menu name must be a menu + * defined in the menu command. This is a blocking command — execution waits for the + * menu to complete before proceeding to the next command. + * + *

Important: this command can only be called within a menu execution context. + */ +@JsonDeserialize(builder = GotoMenuCommandImpl.Builder.class) +public interface GotoMenuCommand extends SvamlCommand { + + /** The command property. Must have the value gotoMenu. */ + public class CommandEnum extends EnumDynamic { + /** The gotoMenu command. */ + public static final CommandEnum GOTO_MENU = new CommandEnum("gotoMenu"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(GOTO_MENU)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Name of the target menu to execute next. Must match a key in menus. + * + *

Field is required + * + * @return menuName + */ + String getMenuName(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new GotoMenuCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param menuName see getter + * @return Current builder + * @see #getMenuName + */ + Builder setMenuName(String menuName); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + GotoMenuCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommandImpl.java new file mode 100644 index 000000000..d5b8e18c2 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommandImpl.java @@ -0,0 +1,122 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + GotoMenuCommandImpl.JSON_PROPERTY_COMMAND, + GotoMenuCommandImpl.JSON_PROPERTY_MENU_NAME +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class GotoMenuCommandImpl implements GotoMenuCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_MENU_NAME = "menuName"; + + private OptionalValue menuName; + + public GotoMenuCommandImpl() {} + + protected GotoMenuCommandImpl( + OptionalValue command, OptionalValue menuName) { + this.command = command; + this.menuName = menuName; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getMenuName() { + return menuName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MENU_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue menuName() { + return menuName; + } + + /** Return true if this GotoMenuCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GotoMenuCommandImpl gotoMenuCommand = (GotoMenuCommandImpl) o; + return Objects.equals(this.command, gotoMenuCommand.command) + && Objects.equals(this.menuName, gotoMenuCommand.menuName); + } + + @Override + public int hashCode() { + return Objects.hash(command, menuName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class GotoMenuCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" menuName: ").append(toIndentedString(menuName)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements GotoMenuCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.GOTO_MENU); + OptionalValue menuName = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.GOTO_MENU)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.GOTO_MENU, command)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_MENU_NAME, required = true) + public Builder setMenuName(String menuName) { + this.menuName = OptionalValue.of(menuName); + return this; + } + + public GotoMenuCommand build() { + return new GotoMenuCommandImpl(command, menuName); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommand.java new file mode 100644 index 000000000..ad922ea41 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommand.java @@ -0,0 +1,84 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Ends a call leg. This is a non-blocking command — execution continues to the next command in the + * sequence even though the call has been ended. The onHangup event is triggered for + * the call leg that was ended. + * + *

Any subsequent commands that target the ended call leg (such as messages or other + * media commands) are valid but will not be executed. Commands that operate independently — such as + * initiating a new call with dial — will execute normally. This makes it possible, for + * example, to end one call and immediately start another within the same sequence. + */ +@JsonDeserialize(builder = HangupCommandImpl.Builder.class) +public interface HangupCommand extends SvamlCommand { + + /** The command property. Must have the value hangup. */ + public class CommandEnum extends EnumDynamic { + /** The hangup command. */ + public static final CommandEnum HANGUP = new CommandEnum("hangup"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(HANGUP)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Name of the call leg to end, as set by callName in the dial command. + * + *

If omitted, the current call leg is ended. + * + * @return callName + */ + String getCallName(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new HangupCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param callName see getter + * @return Current builder + * @see #getCallName + */ + Builder setCallName(String callName); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + HangupCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommandImpl.java new file mode 100644 index 000000000..1902f5daa --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommandImpl.java @@ -0,0 +1,121 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + HangupCommandImpl.JSON_PROPERTY_COMMAND, + HangupCommandImpl.JSON_PROPERTY_CALL_NAME +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class HangupCommandImpl implements HangupCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_CALL_NAME = "callName"; + + private OptionalValue callName; + + public HangupCommandImpl() {} + + protected HangupCommandImpl(OptionalValue command, OptionalValue callName) { + this.command = command; + this.callName = callName; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getCallName() { + return callName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callName() { + return callName; + } + + /** Return true if this HangupCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HangupCommandImpl hangupCommand = (HangupCommandImpl) o; + return Objects.equals(this.command, hangupCommand.command) + && Objects.equals(this.callName, hangupCommand.callName); + } + + @Override + public int hashCode() { + return Objects.hash(command, callName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class HangupCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" callName: ").append(toIndentedString(callName)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements HangupCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.HANGUP); + OptionalValue callName = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.HANGUP)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.HANGUP, command)); + } + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + public Builder setCallName(String callName) { + this.callName = OptionalValue.of(callName); + return this; + } + + public HangupCommand build() { + return new HangupCommandImpl(command, callName); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommand.java new file mode 100644 index 000000000..c102d30d3 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommand.java @@ -0,0 +1,99 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Defines a set of named menus and executes them starting from startMenu. This is a blocking + * command — execution waits for the menu to complete before proceeding to the next command. + * + *

Each menu item configures prompts, input collection, timeout handling, and repeat behavior. + */ +@JsonDeserialize(builder = MenuCommandImpl.Builder.class) +public interface MenuCommand extends SvamlCommand { + + /** The command property. Must have the value menu. */ + public class CommandEnum extends EnumDynamic { + /** The menu command. */ + public static final CommandEnum MENU = new CommandEnum("menu"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(MENU)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Name of the menu to execute first. Must match a key in menus. + * + *

Field is required + * + * @return startMenu + */ + String getStartMenu(); + + /** + * Map of menu definitions keyed by menu name. + * + *

Field is required + * + * @return menus + */ + Map getMenus(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new MenuCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param startMenu see getter + * @return Current builder + * @see #getStartMenu + */ + Builder setStartMenu(String startMenu); + + /** + * see getter + * + * @param menus see getter + * @return Current builder + * @see #getMenus + */ + Builder setMenus(Map menus); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + MenuCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommandImpl.java new file mode 100644 index 000000000..0efa2c529 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommandImpl.java @@ -0,0 +1,151 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Map; +import java.util.Objects; + +@JsonPropertyOrder({ + MenuCommandImpl.JSON_PROPERTY_COMMAND, + MenuCommandImpl.JSON_PROPERTY_START_MENU, + MenuCommandImpl.JSON_PROPERTY_MENUS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class MenuCommandImpl implements MenuCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_START_MENU = "startMenu"; + + private OptionalValue startMenu; + + public static final String JSON_PROPERTY_MENUS = "menus"; + + private OptionalValue> menus; + + public MenuCommandImpl() {} + + protected MenuCommandImpl( + OptionalValue command, + OptionalValue startMenu, + OptionalValue> menus) { + this.command = command; + this.startMenu = startMenu; + this.menus = menus; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getStartMenu() { + return startMenu.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_START_MENU) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue startMenu() { + return startMenu; + } + + @JsonIgnore + public Map getMenus() { + return menus.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MENUS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> menus() { + return menus; + } + + /** Return true if this MenuCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MenuCommandImpl menuCommand = (MenuCommandImpl) o; + return Objects.equals(this.command, menuCommand.command) + && Objects.equals(this.startMenu, menuCommand.startMenu) + && Objects.equals(this.menus, menuCommand.menus); + } + + @Override + public int hashCode() { + return Objects.hash(command, startMenu, menus); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class MenuCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" startMenu: ").append(toIndentedString(startMenu)).append("\n"); + sb.append(" menus: ").append(toIndentedString(menus)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements MenuCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.MENU); + OptionalValue startMenu = OptionalValue.empty(); + OptionalValue> menus = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.MENU)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.MENU, command)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_START_MENU, required = true) + public Builder setStartMenu(String startMenu) { + this.startMenu = OptionalValue.of(startMenu); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_MENUS, required = true) + public Builder setMenus(Map menus) { + this.menus = OptionalValue.of(menus); + return this; + } + + public MenuCommand build() { + return new MenuCommandImpl(command, startMenu, menus); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItem.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItem.java new file mode 100644 index 000000000..2096a82fa --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItem.java @@ -0,0 +1,249 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Defines a single menu step, including prompts, input handling rules, input-to-command matches, + * and failure handling. + * + *

Each collected input is matched against the values in the matches property. If no + * match succeeds, the onFail commands are executed. + * + *

If neither matches nor onFail is defined and the service call + * behavior is set to WEBHOOK, a webhook request is sent including the collected input. + */ +@JsonDeserialize(builder = MenuItemImpl.Builder.class) +public interface MenuItem { + + /** Gets or Sets inputMethods */ + public class InputMethodsEnum extends EnumDynamic { + public static final InputMethodsEnum DTMF = new InputMethodsEnum("DTMF"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + InputMethodsEnum.class, InputMethodsEnum::new, Arrays.asList(DTMF)); + + private InputMethodsEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static InputMethodsEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(InputMethodsEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Prompt played when this menu starts. + * + *

This prompt is also used as the repeat prompt when repeatPrompt is not defined for the menu. + * + * @return prompt + */ + MenuPrompt getPrompt(); + + /** + * Prompt played when the menu is repeated. + * + *

Repeats occur when input times out or when the provided input does not match any menu match + * item. + * + * @return repeatPrompt + */ + MenuPrompt getRepeatPrompt(); + + /** + * Maximum number of seconds to wait for user input before the input attempt times out. + * + * @return inputTimeoutDurationSeconds + */ + Integer getInputTimeoutDurationSeconds(); + + /** + * Maximum number of times the menu is repeated. + * + *

A repeat occurs when input times out or when the provided input does not match any menu + * match item. + * + * @return repeatCount + */ + Integer getRepeatCount(); + + /** + * Minimum number of input characters required before the menu evaluates the collected input. + * + * @return minimumInputLength + */ + Integer getMinimumInputLength(); + + /** + * Maximum number of input characters that triggers the menu to evaluate the collected input. + * + * @return maximumInputLength + */ + Integer getMaximumInputLength(); + + /** + * Character sequence that signals the end of input and triggers immediate evaluation. + * + *

Useful when variable-length input is allowed and shorter valid options should be submitted + * without waiting for timeout or maximum length. + * + *

The terminating sequence value is included in the evaluated input. + * + * @return terminatingSequence + */ + String getTerminatingSequence(); + + /** + * Input methods accepted for this menu when collecting user input. + * + * @return inputMethods + */ + List getInputMethods(); + + /** + * Items matched against the collected input. Maximum number of allowed match expressions is 50. + * + *

Defined as a dictionary where each property name is a literal or a regular expression + * string. + * + *

Values are evaluated in the order they are defined. + * + * @return matches + */ + Map> getMatches(); + + /** + * SVAML commands executed when the menu fails to collect a matching input. + * + *

This handler runs after the repeat limit is reached without any input matching a menu match + * item. + * + * @return onFail + */ + List getOnFail(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new MenuItemImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param prompt see getter + * @return Current builder + * @see #getPrompt + */ + Builder setPrompt(MenuPrompt prompt); + + /** + * see getter + * + * @param repeatPrompt see getter + * @return Current builder + * @see #getRepeatPrompt + */ + Builder setRepeatPrompt(MenuPrompt repeatPrompt); + + /** + * see getter + * + * @param inputTimeoutDurationSeconds see getter + * @return Current builder + * @see #getInputTimeoutDurationSeconds + */ + Builder setInputTimeoutDurationSeconds(Integer inputTimeoutDurationSeconds); + + /** + * see getter + * + * @param repeatCount see getter + * @return Current builder + * @see #getRepeatCount + */ + Builder setRepeatCount(Integer repeatCount); + + /** + * see getter + * + * @param minimumInputLength see getter + * @return Current builder + * @see #getMinimumInputLength + */ + Builder setMinimumInputLength(Integer minimumInputLength); + + /** + * see getter + * + * @param maximumInputLength see getter + * @return Current builder + * @see #getMaximumInputLength + */ + Builder setMaximumInputLength(Integer maximumInputLength); + + /** + * see getter + * + * @param terminatingSequence see getter + * @return Current builder + * @see #getTerminatingSequence + */ + Builder setTerminatingSequence(String terminatingSequence); + + /** + * see getter + * + * @param inputMethods see getter + * @return Current builder + * @see #getInputMethods + */ + Builder setInputMethods(List inputMethods); + + /** + * see getter + * + * @param matches see getter + * @return Current builder + * @see #getMatches + */ + Builder setMatches(Map> matches); + + /** + * see getter + * + * @param onFail see getter + * @return Current builder + * @see #getOnFail + */ + Builder setOnFail(List onFail); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + MenuItem build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItemImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItemImpl.java new file mode 100644 index 000000000..1968a8488 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItemImpl.java @@ -0,0 +1,363 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@JsonPropertyOrder({ + MenuItemImpl.JSON_PROPERTY_PROMPT, + MenuItemImpl.JSON_PROPERTY_REPEAT_PROMPT, + MenuItemImpl.JSON_PROPERTY_INPUT_TIMEOUT_DURATION_SECONDS, + MenuItemImpl.JSON_PROPERTY_REPEAT_COUNT, + MenuItemImpl.JSON_PROPERTY_MINIMUM_INPUT_LENGTH, + MenuItemImpl.JSON_PROPERTY_MAXIMUM_INPUT_LENGTH, + MenuItemImpl.JSON_PROPERTY_TERMINATING_SEQUENCE, + MenuItemImpl.JSON_PROPERTY_INPUT_METHODS, + MenuItemImpl.JSON_PROPERTY_MATCHES, + MenuItemImpl.JSON_PROPERTY_ON_FAIL +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class MenuItemImpl implements MenuItem { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_PROMPT = "prompt"; + + private OptionalValue prompt; + + public static final String JSON_PROPERTY_REPEAT_PROMPT = "repeatPrompt"; + + private OptionalValue repeatPrompt; + + public static final String JSON_PROPERTY_INPUT_TIMEOUT_DURATION_SECONDS = + "inputTimeoutDurationSeconds"; + + private OptionalValue inputTimeoutDurationSeconds; + + public static final String JSON_PROPERTY_REPEAT_COUNT = "repeatCount"; + + private OptionalValue repeatCount; + + public static final String JSON_PROPERTY_MINIMUM_INPUT_LENGTH = "minimumInputLength"; + + private OptionalValue minimumInputLength; + + public static final String JSON_PROPERTY_MAXIMUM_INPUT_LENGTH = "maximumInputLength"; + + private OptionalValue maximumInputLength; + + public static final String JSON_PROPERTY_TERMINATING_SEQUENCE = "terminatingSequence"; + + private OptionalValue terminatingSequence; + + public static final String JSON_PROPERTY_INPUT_METHODS = "inputMethods"; + + private OptionalValue> inputMethods; + + public static final String JSON_PROPERTY_MATCHES = "matches"; + + private OptionalValue>> matches; + + public static final String JSON_PROPERTY_ON_FAIL = "onFail"; + + private OptionalValue> onFail; + + public MenuItemImpl() {} + + protected MenuItemImpl( + OptionalValue prompt, + OptionalValue repeatPrompt, + OptionalValue inputTimeoutDurationSeconds, + OptionalValue repeatCount, + OptionalValue minimumInputLength, + OptionalValue maximumInputLength, + OptionalValue terminatingSequence, + OptionalValue> inputMethods, + OptionalValue>> matches, + OptionalValue> onFail) { + this.prompt = prompt; + this.repeatPrompt = repeatPrompt; + this.inputTimeoutDurationSeconds = inputTimeoutDurationSeconds; + this.repeatCount = repeatCount; + this.minimumInputLength = minimumInputLength; + this.maximumInputLength = maximumInputLength; + this.terminatingSequence = terminatingSequence; + this.inputMethods = inputMethods; + this.matches = matches; + this.onFail = onFail; + } + + @JsonIgnore + public MenuPrompt getPrompt() { + return prompt.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PROMPT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue prompt() { + return prompt; + } + + @JsonIgnore + public MenuPrompt getRepeatPrompt() { + return repeatPrompt.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_REPEAT_PROMPT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue repeatPrompt() { + return repeatPrompt; + } + + @JsonIgnore + public Integer getInputTimeoutDurationSeconds() { + return inputTimeoutDurationSeconds.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_INPUT_TIMEOUT_DURATION_SECONDS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue inputTimeoutDurationSeconds() { + return inputTimeoutDurationSeconds; + } + + @JsonIgnore + public Integer getRepeatCount() { + return repeatCount.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_REPEAT_COUNT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue repeatCount() { + return repeatCount; + } + + @JsonIgnore + public Integer getMinimumInputLength() { + return minimumInputLength.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MINIMUM_INPUT_LENGTH) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue minimumInputLength() { + return minimumInputLength; + } + + @JsonIgnore + public Integer getMaximumInputLength() { + return maximumInputLength.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MAXIMUM_INPUT_LENGTH) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue maximumInputLength() { + return maximumInputLength; + } + + @JsonIgnore + public String getTerminatingSequence() { + return terminatingSequence.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TERMINATING_SEQUENCE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue terminatingSequence() { + return terminatingSequence; + } + + @JsonIgnore + public List getInputMethods() { + return inputMethods.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_INPUT_METHODS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> inputMethods() { + return inputMethods; + } + + @JsonIgnore + public Map> getMatches() { + return matches.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MATCHES) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue>> matches() { + return matches; + } + + @JsonIgnore + public List getOnFail() { + return onFail.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_FAIL) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onFail() { + return onFail; + } + + /** Return true if this MenuItem object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MenuItemImpl menuItem = (MenuItemImpl) o; + return Objects.equals(this.prompt, menuItem.prompt) + && Objects.equals(this.repeatPrompt, menuItem.repeatPrompt) + && Objects.equals(this.inputTimeoutDurationSeconds, menuItem.inputTimeoutDurationSeconds) + && Objects.equals(this.repeatCount, menuItem.repeatCount) + && Objects.equals(this.minimumInputLength, menuItem.minimumInputLength) + && Objects.equals(this.maximumInputLength, menuItem.maximumInputLength) + && Objects.equals(this.terminatingSequence, menuItem.terminatingSequence) + && Objects.equals(this.inputMethods, menuItem.inputMethods) + && Objects.equals(this.matches, menuItem.matches) + && Objects.equals(this.onFail, menuItem.onFail); + } + + @Override + public int hashCode() { + return Objects.hash( + prompt, + repeatPrompt, + inputTimeoutDurationSeconds, + repeatCount, + minimumInputLength, + maximumInputLength, + terminatingSequence, + inputMethods, + matches, + onFail); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class MenuItemImpl {\n"); + sb.append(" prompt: ").append(toIndentedString(prompt)).append("\n"); + sb.append(" repeatPrompt: ").append(toIndentedString(repeatPrompt)).append("\n"); + sb.append(" inputTimeoutDurationSeconds: ") + .append(toIndentedString(inputTimeoutDurationSeconds)) + .append("\n"); + sb.append(" repeatCount: ").append(toIndentedString(repeatCount)).append("\n"); + sb.append(" minimumInputLength: ").append(toIndentedString(minimumInputLength)).append("\n"); + sb.append(" maximumInputLength: ").append(toIndentedString(maximumInputLength)).append("\n"); + sb.append(" terminatingSequence: ") + .append(toIndentedString(terminatingSequence)) + .append("\n"); + sb.append(" inputMethods: ").append(toIndentedString(inputMethods)).append("\n"); + sb.append(" matches: ").append(toIndentedString(matches)).append("\n"); + sb.append(" onFail: ").append(toIndentedString(onFail)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements MenuItem.Builder { + OptionalValue prompt = OptionalValue.empty(); + OptionalValue repeatPrompt = OptionalValue.empty(); + OptionalValue inputTimeoutDurationSeconds = OptionalValue.empty(); + OptionalValue repeatCount = OptionalValue.empty(); + OptionalValue minimumInputLength = OptionalValue.empty(); + OptionalValue maximumInputLength = OptionalValue.empty(); + OptionalValue terminatingSequence = OptionalValue.empty(); + OptionalValue> inputMethods = OptionalValue.empty(); + OptionalValue>> matches = OptionalValue.empty(); + OptionalValue> onFail = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_PROMPT) + public Builder setPrompt(MenuPrompt prompt) { + this.prompt = OptionalValue.of(prompt); + return this; + } + + @JsonProperty(JSON_PROPERTY_REPEAT_PROMPT) + public Builder setRepeatPrompt(MenuPrompt repeatPrompt) { + this.repeatPrompt = OptionalValue.of(repeatPrompt); + return this; + } + + @JsonProperty(JSON_PROPERTY_INPUT_TIMEOUT_DURATION_SECONDS) + public Builder setInputTimeoutDurationSeconds(Integer inputTimeoutDurationSeconds) { + this.inputTimeoutDurationSeconds = OptionalValue.of(inputTimeoutDurationSeconds); + return this; + } + + @JsonProperty(JSON_PROPERTY_REPEAT_COUNT) + public Builder setRepeatCount(Integer repeatCount) { + this.repeatCount = OptionalValue.of(repeatCount); + return this; + } + + @JsonProperty(JSON_PROPERTY_MINIMUM_INPUT_LENGTH) + public Builder setMinimumInputLength(Integer minimumInputLength) { + this.minimumInputLength = OptionalValue.of(minimumInputLength); + return this; + } + + @JsonProperty(JSON_PROPERTY_MAXIMUM_INPUT_LENGTH) + public Builder setMaximumInputLength(Integer maximumInputLength) { + this.maximumInputLength = OptionalValue.of(maximumInputLength); + return this; + } + + @JsonProperty(JSON_PROPERTY_TERMINATING_SEQUENCE) + public Builder setTerminatingSequence(String terminatingSequence) { + this.terminatingSequence = OptionalValue.of(terminatingSequence); + return this; + } + + @JsonProperty(JSON_PROPERTY_INPUT_METHODS) + public Builder setInputMethods(List inputMethods) { + this.inputMethods = OptionalValue.of(inputMethods); + return this; + } + + @JsonProperty(JSON_PROPERTY_MATCHES) + public Builder setMatches(Map> matches) { + this.matches = OptionalValue.of(matches); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_FAIL) + public Builder setOnFail(List onFail) { + this.onFail = OptionalValue.of(onFail); + return this; + } + + public MenuItem build() { + return new MenuItemImpl( + prompt, + repeatPrompt, + inputTimeoutDurationSeconds, + repeatCount, + minimumInputLength, + maximumInputLength, + terminatingSequence, + inputMethods, + matches, + onFail); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPrompt.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPrompt.java new file mode 100644 index 000000000..c72f308ea --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPrompt.java @@ -0,0 +1,69 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** Prompt configuration for menu playback, including prompt messages and barge-in behavior. */ +@JsonDeserialize(builder = MenuPromptImpl.Builder.class) +public interface MenuPrompt { + + /** + * Controls whether input can interrupt prompt playback. + * + *

When enabled, playback stops as soon as input is detected and the input is evaluated + * immediately if matching conditions are met. + * + *

When disabled, input is still collected during playback and evaluated after playback + * finishes. + * + * @return allowBargeIn + */ + Boolean getAllowBargeIn(); + + /** + * Ordered list of messages to play. + * + *

Field is required + * + * @return messages + */ + List getMessages(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new MenuPromptImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param allowBargeIn see getter + * @return Current builder + * @see #getAllowBargeIn + */ + Builder setAllowBargeIn(Boolean allowBargeIn); + + /** + * see getter + * + * @param messages see getter + * @return Current builder + * @see #getMessages + */ + Builder setMessages(List messages); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + MenuPrompt build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPromptImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPromptImpl.java new file mode 100644 index 000000000..34e67b573 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPromptImpl.java @@ -0,0 +1,120 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + MenuPromptImpl.JSON_PROPERTY_ALLOW_BARGE_IN, + MenuPromptImpl.JSON_PROPERTY_MESSAGES +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class MenuPromptImpl implements MenuPrompt { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ALLOW_BARGE_IN = "allowBargeIn"; + + private OptionalValue allowBargeIn; + + public static final String JSON_PROPERTY_MESSAGES = "messages"; + + private OptionalValue> messages; + + public MenuPromptImpl() {} + + protected MenuPromptImpl( + OptionalValue allowBargeIn, OptionalValue> messages) { + this.allowBargeIn = allowBargeIn; + this.messages = messages; + } + + @JsonIgnore + public Boolean getAllowBargeIn() { + return allowBargeIn.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ALLOW_BARGE_IN) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue allowBargeIn() { + return allowBargeIn; + } + + @JsonIgnore + public List getMessages() { + return messages.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MESSAGES) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> messages() { + return messages; + } + + /** Return true if this MenuPrompt object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MenuPromptImpl menuPrompt = (MenuPromptImpl) o; + return Objects.equals(this.allowBargeIn, menuPrompt.allowBargeIn) + && Objects.equals(this.messages, menuPrompt.messages); + } + + @Override + public int hashCode() { + return Objects.hash(allowBargeIn, messages); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class MenuPromptImpl {\n"); + sb.append(" allowBargeIn: ").append(toIndentedString(allowBargeIn)).append("\n"); + sb.append(" messages: ").append(toIndentedString(messages)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements MenuPrompt.Builder { + OptionalValue allowBargeIn = OptionalValue.empty(); + OptionalValue> messages = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_ALLOW_BARGE_IN) + public Builder setAllowBargeIn(Boolean allowBargeIn) { + this.allowBargeIn = OptionalValue.of(allowBargeIn); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_MESSAGES, required = true) + public Builder setMessages(List messages) { + this.messages = OptionalValue.of(messages); + return this; + } + + public MenuPrompt build() { + return new MenuPromptImpl(allowBargeIn, messages); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEvents.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEvents.java new file mode 100644 index 000000000..2966e836f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEvents.java @@ -0,0 +1,45 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** SVAML commands to execute based on message playback outcomes. */ +@JsonDeserialize(builder = MessageEventsImpl.Builder.class) +public interface MessageEvents { + + /** + * Commands to execute when all messages in the sequence have finished playing. + * + * @return onFinish + */ + List getOnFinish(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new MessageEventsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param onFinish see getter + * @return Current builder + * @see #getOnFinish + */ + Builder setOnFinish(List onFinish); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + MessageEvents build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEventsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEventsImpl.java new file mode 100644 index 000000000..10c7eb64f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEventsImpl.java @@ -0,0 +1,91 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({MessageEventsImpl.JSON_PROPERTY_ON_FINISH}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class MessageEventsImpl implements MessageEvents { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ON_FINISH = "onFinish"; + + private OptionalValue> onFinish; + + public MessageEventsImpl() {} + + protected MessageEventsImpl(OptionalValue> onFinish) { + this.onFinish = onFinish; + } + + @JsonIgnore + public List getOnFinish() { + return onFinish.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_FINISH) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onFinish() { + return onFinish; + } + + /** Return true if this MessageEvents object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MessageEventsImpl messageEvents = (MessageEventsImpl) o; + return Objects.equals(this.onFinish, messageEvents.onFinish); + } + + @Override + public int hashCode() { + return Objects.hash(onFinish); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class MessageEventsImpl {\n"); + sb.append(" onFinish: ").append(toIndentedString(onFinish)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements MessageEvents.Builder { + OptionalValue> onFinish = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_ON_FINISH) + public Builder setOnFinish(List onFinish) { + this.onFinish = OptionalValue.of(onFinish); + return this; + } + + public MessageEvents build() { + return new MessageEventsImpl(onFinish); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommand.java new file mode 100644 index 000000000..68c6c1638 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommand.java @@ -0,0 +1,119 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +/** + * Plays one or more messages on the call. Multiple messages in the array are played sequentially in + * order. + * + *

This is a non-blocking command — the next command in the sequence executes immediately while + * messages play in parallel. Playback outcomes are handled via the events property. + * The onFinish event can be used to run a command after all messages complete. + */ +@JsonDeserialize(builder = MessagesCommandImpl.Builder.class) +public interface MessagesCommand extends SvamlCommand { + + /** The command property. Must have the value messages. */ + public class CommandEnum extends EnumDynamic { + /** The messages command. */ + public static final CommandEnum MESSAGES = new CommandEnum("messages"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(MESSAGES)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Name of the message for identification and reference within the call session. + * + *

This name is used to uniquely identify the message and must be unique within the current + * call session. This name can be referenced in other commands (e.g., stopMessages) + * to control this specific message. + * + * @return messagesName + */ + String getMessagesName(); + + /** + * Ordered list of messages to play. + * + *

Field is required + * + * @return messages + */ + List getMessages(); + + /** + * SVAML commands to execute based on message playback outcomes. + * + * @return events + */ + MessageEvents getEvents(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new MessagesCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param messagesName see getter + * @return Current builder + * @see #getMessagesName + */ + Builder setMessagesName(String messagesName); + + /** + * see getter + * + * @param messages see getter + * @return Current builder + * @see #getMessages + */ + Builder setMessages(List messages); + + /** + * see getter + * + * @param events see getter + * @return Current builder + * @see #getEvents + */ + Builder setEvents(MessageEvents events); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + MessagesCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommandImpl.java new file mode 100644 index 000000000..4d53e0176 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommandImpl.java @@ -0,0 +1,178 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + MessagesCommandImpl.JSON_PROPERTY_COMMAND, + MessagesCommandImpl.JSON_PROPERTY_MESSAGES_NAME, + MessagesCommandImpl.JSON_PROPERTY_MESSAGES, + MessagesCommandImpl.JSON_PROPERTY_EVENTS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class MessagesCommandImpl implements MessagesCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_MESSAGES_NAME = "messagesName"; + + private OptionalValue messagesName; + + public static final String JSON_PROPERTY_MESSAGES = "messages"; + + private OptionalValue> messages; + + public static final String JSON_PROPERTY_EVENTS = "events"; + + private OptionalValue events; + + public MessagesCommandImpl() {} + + protected MessagesCommandImpl( + OptionalValue command, + OptionalValue messagesName, + OptionalValue> messages, + OptionalValue events) { + this.command = command; + this.messagesName = messagesName; + this.messages = messages; + this.events = events; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getMessagesName() { + return messagesName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MESSAGES_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue messagesName() { + return messagesName; + } + + @JsonIgnore + public List getMessages() { + return messages.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MESSAGES) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> messages() { + return messages; + } + + @JsonIgnore + public MessageEvents getEvents() { + return events.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue events() { + return events; + } + + /** Return true if this MessagesCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MessagesCommandImpl messagesCommand = (MessagesCommandImpl) o; + return Objects.equals(this.command, messagesCommand.command) + && Objects.equals(this.messagesName, messagesCommand.messagesName) + && Objects.equals(this.messages, messagesCommand.messages) + && Objects.equals(this.events, messagesCommand.events); + } + + @Override + public int hashCode() { + return Objects.hash(command, messagesName, messages, events); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class MessagesCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" messagesName: ").append(toIndentedString(messagesName)).append("\n"); + sb.append(" messages: ").append(toIndentedString(messages)).append("\n"); + sb.append(" events: ").append(toIndentedString(events)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements MessagesCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.MESSAGES); + OptionalValue messagesName = OptionalValue.empty(); + OptionalValue> messages = OptionalValue.empty(); + OptionalValue events = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.MESSAGES)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.MESSAGES, command)); + } + return this; + } + + @JsonProperty(JSON_PROPERTY_MESSAGES_NAME) + public Builder setMessagesName(String messagesName) { + this.messagesName = OptionalValue.of(messagesName); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_MESSAGES, required = true) + public Builder setMessages(List messages) { + this.messages = OptionalValue.of(messages); + return this; + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + public Builder setEvents(MessageEvents events) { + this.events = OptionalValue.of(events); + return this; + } + + public MessagesCommand build() { + return new MessagesCommandImpl(command, messagesName, messages, events); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommand.java new file mode 100644 index 000000000..5930751fc --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommand.java @@ -0,0 +1,81 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Delays execution of the next command in the sequence for a specified duration. This is a blocking + * command — no further commands execute until the pause completes. + * + *

The pause does not affect call audio; the call remains connected and audio continues + * uninterrupted. + */ +@JsonDeserialize(builder = PauseCommandImpl.Builder.class) +public interface PauseCommand extends SvamlCommand { + + /** The command property. Must have the value pause. */ + public class CommandEnum extends EnumDynamic { + /** The pause command. */ + public static final CommandEnum PAUSE = new CommandEnum("pause"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(PAUSE)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Duration of the pause in milliseconds. + * + *

Field is required + * + * @return durationMilliseconds + */ + Integer getDurationMilliseconds(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new PauseCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param durationMilliseconds see getter + * @return Current builder + * @see #getDurationMilliseconds + */ + Builder setDurationMilliseconds(Integer durationMilliseconds); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + PauseCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommandImpl.java new file mode 100644 index 000000000..cf813313d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommandImpl.java @@ -0,0 +1,124 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + PauseCommandImpl.JSON_PROPERTY_COMMAND, + PauseCommandImpl.JSON_PROPERTY_DURATION_MILLISECONDS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class PauseCommandImpl implements PauseCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_DURATION_MILLISECONDS = "durationMilliseconds"; + + private OptionalValue durationMilliseconds; + + public PauseCommandImpl() {} + + protected PauseCommandImpl( + OptionalValue command, OptionalValue durationMilliseconds) { + this.command = command; + this.durationMilliseconds = durationMilliseconds; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public Integer getDurationMilliseconds() { + return durationMilliseconds.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DURATION_MILLISECONDS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue durationMilliseconds() { + return durationMilliseconds; + } + + /** Return true if this PauseCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PauseCommandImpl pauseCommand = (PauseCommandImpl) o; + return Objects.equals(this.command, pauseCommand.command) + && Objects.equals(this.durationMilliseconds, pauseCommand.durationMilliseconds); + } + + @Override + public int hashCode() { + return Objects.hash(command, durationMilliseconds); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PauseCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" durationMilliseconds: ") + .append(toIndentedString(durationMilliseconds)) + .append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements PauseCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.PAUSE); + OptionalValue durationMilliseconds = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.PAUSE)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.PAUSE, command)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_DURATION_MILLISECONDS, required = true) + public Builder setDurationMilliseconds(Integer durationMilliseconds) { + this.durationMilliseconds = OptionalValue.of(durationMilliseconds); + return this; + } + + public PauseCommand build() { + return new PauseCommandImpl(command, durationMilliseconds); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Play.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Play.java new file mode 100644 index 000000000..5eed77b9e --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Play.java @@ -0,0 +1,45 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder = PlayImpl.Builder.class) +public interface Play { + + /** + * URL of the media to send + * + *

Field is required + * + * @return url + */ + String getUrl(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new PlayImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param url see getter + * @return Current builder + * @see #getUrl + */ + Builder setUrl(String url); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + Play build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayImpl.java new file mode 100644 index 000000000..a81f2c2f3 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayImpl.java @@ -0,0 +1,90 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({PlayImpl.JSON_PROPERTY_URL}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class PlayImpl implements Play { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_URL = "url"; + + private OptionalValue url; + + public PlayImpl() {} + + protected PlayImpl(OptionalValue url) { + this.url = url; + } + + @JsonIgnore + public String getUrl() { + return url.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_URL) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue url() { + return url; + } + + /** Return true if this Play object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlayImpl play = (PlayImpl) o; + return Objects.equals(this.url, play.url); + } + + @Override + public int hashCode() { + return Objects.hash(url); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PlayImpl {\n"); + sb.append(" url: ").append(toIndentedString(url)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements Play.Builder { + OptionalValue url = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_URL, required = true) + public Builder setUrl(String url) { + this.url = OptionalValue.of(url); + return this; + } + + public Play build() { + return new PlayImpl(url); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessage.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessage.java new file mode 100644 index 000000000..9a7b24241 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessage.java @@ -0,0 +1,78 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * An audio file playback message item. The platform fetches and plays the audio file at the + * provided URL on the call. + */ +@JsonDeserialize(builder = PlayMessageImpl.Builder.class) +public interface PlayMessage extends Message { + + /** The type property. Must have the value PLAY. */ + public class TypeEnum extends EnumDynamic { + /** The PLAY type. */ + public static final TypeEnum PLAY = new TypeEnum("PLAY"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(PLAY)); + + private TypeEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Get play + * + *

Field is required + * + * @return play + */ + Play getPlay(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new PlayMessageImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param play see getter + * @return Current builder + * @see #getPlay + */ + Builder setPlay(Play play); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + PlayMessage build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessageImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessageImpl.java new file mode 100644 index 000000000..29b63a438 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessageImpl.java @@ -0,0 +1,118 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({PlayMessageImpl.JSON_PROPERTY_TYPE, PlayMessageImpl.JSON_PROPERTY_PLAY}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class PlayMessageImpl implements PlayMessage, Message { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_PLAY = "play"; + + private OptionalValue play; + + public PlayMessageImpl() {} + + protected PlayMessageImpl(OptionalValue type, OptionalValue play) { + this.type = type; + this.play = play; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public Play getPlay() { + return play.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PLAY) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue play() { + return play; + } + + /** Return true if this PlayMessage object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PlayMessageImpl playMessage = (PlayMessageImpl) o; + return Objects.equals(this.type, playMessage.type) + && Objects.equals(this.play, playMessage.play); + } + + @Override + public int hashCode() { + return Objects.hash(type, play); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PlayMessageImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" play: ").append(toIndentedString(play)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements PlayMessage.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.PLAY); + OptionalValue play = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.PLAY)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.PLAY, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_PLAY, required = true) + public Builder setPlay(Play play) { + this.play = OptionalValue.of(play); + return this; + } + + public PlayMessage build() { + return new PlayMessageImpl(type, play); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingDestinationType.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingDestinationType.java new file mode 100644 index 000000000..f5a571103 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingDestinationType.java @@ -0,0 +1,46 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** Select target service to receive recorded and transcribed files */ +public class RecordingDestinationType extends EnumDynamic { + /** + * Store recordings in Amazon Web Services S3 bucket. Use s3 as schema for the URL. + */ + public static final RecordingDestinationType AWS = new RecordingDestinationType("AWS"); + + /** + * Store recordings in Google Cloud Platform Storage. Use gs as schema for the URL. + */ + public static final RecordingDestinationType GCP = new RecordingDestinationType("GCP"); + + /** + * Store recordings in Microsoft Azure Blob Storage. Use azure as schema for the URL. + */ + public static final RecordingDestinationType AZURE = new RecordingDestinationType("AZURE"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + RecordingDestinationType.class, + RecordingDestinationType::new, + Arrays.asList(AWS, GCP, AZURE)); + + private RecordingDestinationType(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static RecordingDestinationType from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(RecordingDestinationType e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEvents.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEvents.java new file mode 100644 index 000000000..71008229b --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEvents.java @@ -0,0 +1,63 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** SVAML commands to execute based on recording lifecycle outcomes. */ +@JsonDeserialize(builder = RecordingEventsImpl.Builder.class) +public interface RecordingEvents { + + /** + * Commands to execute when the recording is successfully stopped. Note that this does not mean + * that the file is delivered to the configured destination yet. + * + * @return onFinish + */ + List getOnFinish(); + + /** + * Commands to execute if the recording fails to start. If omitted, failures are silently ignored + * and the call flow continues. + * + * @return onFailure + */ + List getOnFailure(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new RecordingEventsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param onFinish see getter + * @return Current builder + * @see #getOnFinish + */ + Builder setOnFinish(List onFinish); + + /** + * see getter + * + * @param onFailure see getter + * @return Current builder + * @see #getOnFailure + */ + Builder setOnFailure(List onFailure); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + RecordingEvents build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEventsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEventsImpl.java new file mode 100644 index 000000000..813627fd3 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEventsImpl.java @@ -0,0 +1,120 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + RecordingEventsImpl.JSON_PROPERTY_ON_FINISH, + RecordingEventsImpl.JSON_PROPERTY_ON_FAILURE +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class RecordingEventsImpl implements RecordingEvents { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ON_FINISH = "onFinish"; + + private OptionalValue> onFinish; + + public static final String JSON_PROPERTY_ON_FAILURE = "onFailure"; + + private OptionalValue> onFailure; + + public RecordingEventsImpl() {} + + protected RecordingEventsImpl( + OptionalValue> onFinish, OptionalValue> onFailure) { + this.onFinish = onFinish; + this.onFailure = onFailure; + } + + @JsonIgnore + public List getOnFinish() { + return onFinish.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_FINISH) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onFinish() { + return onFinish; + } + + @JsonIgnore + public List getOnFailure() { + return onFailure.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_FAILURE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onFailure() { + return onFailure; + } + + /** Return true if this RecordingEvents object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RecordingEventsImpl recordingEvents = (RecordingEventsImpl) o; + return Objects.equals(this.onFinish, recordingEvents.onFinish) + && Objects.equals(this.onFailure, recordingEvents.onFailure); + } + + @Override + public int hashCode() { + return Objects.hash(onFinish, onFailure); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class RecordingEventsImpl {\n"); + sb.append(" onFinish: ").append(toIndentedString(onFinish)).append("\n"); + sb.append(" onFailure: ").append(toIndentedString(onFailure)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements RecordingEvents.Builder { + OptionalValue> onFinish = OptionalValue.empty(); + OptionalValue> onFailure = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_ON_FINISH) + public Builder setOnFinish(List onFinish) { + this.onFinish = OptionalValue.of(onFinish); + return this; + } + + @JsonProperty(JSON_PROPERTY_ON_FAILURE) + public Builder setOnFailure(List onFailure) { + this.onFailure = OptionalValue.of(onFailure); + return this; + } + + public RecordingEvents build() { + return new RecordingEventsImpl(onFinish, onFailure); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingFormatType.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingFormatType.java new file mode 100644 index 000000000..97160f2a6 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingFormatType.java @@ -0,0 +1,35 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** Audio format for this recording. */ +public class RecordingFormatType extends EnumDynamic { + /** MPEG Audio Layer III compressed audio format. */ + public static final RecordingFormatType MP3 = new RecordingFormatType("MP3"); + + /** Waveform Audio File Format, uncompressed audio. */ + public static final RecordingFormatType WAV = new RecordingFormatType("WAV"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + RecordingFormatType.class, RecordingFormatType::new, Arrays.asList(MP3, WAV)); + + private RecordingFormatType(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static RecordingFormatType from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(RecordingFormatType e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptions.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptions.java new file mode 100644 index 000000000..cd2904039 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptions.java @@ -0,0 +1,130 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** Recording options for this recording. */ +@JsonDeserialize(builder = RecordingOptionsImpl.Builder.class) +public interface RecordingOptions { + + /** + * Audio format for this recording. + * + * @return format + */ + RecordingFormatType getFormat(); + + /** + * The type of recording to perform. + * + * @return recordingType + */ + RecordingType getRecordingType(); + + /** + * Select target service to receive recorded and transcribed files + * + *

Field is required + * + * @return destination + */ + RecordingDestinationType getDestination(); + + /** + * Destination URL for the recording. + * + *

Field is required + * + * @return destinationUrl + */ + String getDestinationUrl(); + + /** + * Credentials to third party storage. + * + *

Field is required + * + * @return credentials + */ + String getCredentials(); + + /** + * Configuration for automatic speech-to-text transcription of the recording. + * + * @return transcriptionOptions + */ + TranscriptionOptions getTranscriptionOptions(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new RecordingOptionsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param format see getter + * @return Current builder + * @see #getFormat + */ + Builder setFormat(RecordingFormatType format); + + /** + * see getter + * + * @param recordingType see getter + * @return Current builder + * @see #getRecordingType + */ + Builder setRecordingType(RecordingType recordingType); + + /** + * see getter + * + * @param destination see getter + * @return Current builder + * @see #getDestination + */ + Builder setDestination(RecordingDestinationType destination); + + /** + * see getter + * + * @param destinationUrl see getter + * @return Current builder + * @see #getDestinationUrl + */ + Builder setDestinationUrl(String destinationUrl); + + /** + * see getter + * + * @param credentials see getter + * @return Current builder + * @see #getCredentials + */ + Builder setCredentials(String credentials); + + /** + * see getter + * + * @param transcriptionOptions see getter + * @return Current builder + * @see #getTranscriptionOptions + */ + Builder setTranscriptionOptions(TranscriptionOptions transcriptionOptions); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + RecordingOptions build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptionsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptionsImpl.java new file mode 100644 index 000000000..4b12bae90 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptionsImpl.java @@ -0,0 +1,232 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + RecordingOptionsImpl.JSON_PROPERTY_FORMAT, + RecordingOptionsImpl.JSON_PROPERTY_RECORDING_TYPE, + RecordingOptionsImpl.JSON_PROPERTY_DESTINATION, + RecordingOptionsImpl.JSON_PROPERTY_DESTINATION_URL, + RecordingOptionsImpl.JSON_PROPERTY_CREDENTIALS, + RecordingOptionsImpl.JSON_PROPERTY_TRANSCRIPTION_OPTIONS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class RecordingOptionsImpl implements RecordingOptions { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_FORMAT = "format"; + + private OptionalValue format; + + public static final String JSON_PROPERTY_RECORDING_TYPE = "recordingType"; + + private OptionalValue recordingType; + + public static final String JSON_PROPERTY_DESTINATION = "destination"; + + private OptionalValue destination; + + public static final String JSON_PROPERTY_DESTINATION_URL = "destinationUrl"; + + private OptionalValue destinationUrl; + + public static final String JSON_PROPERTY_CREDENTIALS = "credentials"; + + private OptionalValue credentials; + + public static final String JSON_PROPERTY_TRANSCRIPTION_OPTIONS = "transcriptionOptions"; + + private OptionalValue transcriptionOptions; + + public RecordingOptionsImpl() {} + + protected RecordingOptionsImpl( + OptionalValue format, + OptionalValue recordingType, + OptionalValue destination, + OptionalValue destinationUrl, + OptionalValue credentials, + OptionalValue transcriptionOptions) { + this.format = format; + this.recordingType = recordingType; + this.destination = destination; + this.destinationUrl = destinationUrl; + this.credentials = credentials; + this.transcriptionOptions = transcriptionOptions; + } + + @JsonIgnore + public RecordingFormatType getFormat() { + return format.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_FORMAT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue format() { + return format; + } + + @JsonIgnore + public RecordingType getRecordingType() { + return recordingType.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_RECORDING_TYPE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue recordingType() { + return recordingType; + } + + @JsonIgnore + public RecordingDestinationType getDestination() { + return destination.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DESTINATION) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue destination() { + return destination; + } + + @JsonIgnore + public String getDestinationUrl() { + return destinationUrl.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DESTINATION_URL) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue destinationUrl() { + return destinationUrl; + } + + @JsonIgnore + public String getCredentials() { + return credentials.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CREDENTIALS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue credentials() { + return credentials; + } + + @JsonIgnore + public TranscriptionOptions getTranscriptionOptions() { + return transcriptionOptions.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TRANSCRIPTION_OPTIONS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue transcriptionOptions() { + return transcriptionOptions; + } + + /** Return true if this RecordingOptions object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RecordingOptionsImpl recordingOptions = (RecordingOptionsImpl) o; + return Objects.equals(this.format, recordingOptions.format) + && Objects.equals(this.recordingType, recordingOptions.recordingType) + && Objects.equals(this.destination, recordingOptions.destination) + && Objects.equals(this.destinationUrl, recordingOptions.destinationUrl) + && Objects.equals(this.credentials, recordingOptions.credentials) + && Objects.equals(this.transcriptionOptions, recordingOptions.transcriptionOptions); + } + + @Override + public int hashCode() { + return Objects.hash( + format, recordingType, destination, destinationUrl, credentials, transcriptionOptions); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class RecordingOptionsImpl {\n"); + sb.append(" format: ").append(toIndentedString(format)).append("\n"); + sb.append(" recordingType: ").append(toIndentedString(recordingType)).append("\n"); + sb.append(" destination: ").append(toIndentedString(destination)).append("\n"); + sb.append(" destinationUrl: ").append(toIndentedString(destinationUrl)).append("\n"); + sb.append(" credentials: ").append(toIndentedString(credentials)).append("\n"); + sb.append(" transcriptionOptions: ") + .append(toIndentedString(transcriptionOptions)) + .append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements RecordingOptions.Builder { + OptionalValue format = OptionalValue.empty(); + OptionalValue recordingType = OptionalValue.empty(); + OptionalValue destination = OptionalValue.empty(); + OptionalValue destinationUrl = OptionalValue.empty(); + OptionalValue credentials = OptionalValue.empty(); + OptionalValue transcriptionOptions = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_FORMAT) + public Builder setFormat(RecordingFormatType format) { + this.format = OptionalValue.of(format); + return this; + } + + @JsonProperty(JSON_PROPERTY_RECORDING_TYPE) + public Builder setRecordingType(RecordingType recordingType) { + this.recordingType = OptionalValue.of(recordingType); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_DESTINATION, required = true) + public Builder setDestination(RecordingDestinationType destination) { + this.destination = OptionalValue.of(destination); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_DESTINATION_URL, required = true) + public Builder setDestinationUrl(String destinationUrl) { + this.destinationUrl = OptionalValue.of(destinationUrl); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CREDENTIALS, required = true) + public Builder setCredentials(String credentials) { + this.credentials = OptionalValue.of(credentials); + return this; + } + + @JsonProperty(JSON_PROPERTY_TRANSCRIPTION_OPTIONS) + public Builder setTranscriptionOptions(TranscriptionOptions transcriptionOptions) { + this.transcriptionOptions = OptionalValue.of(transcriptionOptions); + return this; + } + + public RecordingOptions build() { + return new RecordingOptionsImpl( + format, recordingType, destination, destinationUrl, credentials, transcriptionOptions); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingType.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingType.java new file mode 100644 index 000000000..b6a87d5de --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingType.java @@ -0,0 +1,38 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** The type of recording to perform. */ +public class RecordingType extends EnumDynamic { + /** Record inbound and outbound voice streams. */ + public static final RecordingType COMBINED = new RecordingType("COMBINED"); + + /** Record inbound voice stream only. */ + public static final RecordingType INBOUND = new RecordingType("INBOUND"); + + /** Record outbound voice stream only. */ + public static final RecordingType OUTBOUND = new RecordingType("OUTBOUND"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + RecordingType.class, RecordingType::new, Arrays.asList(COMBINED, INBOUND, OUTBOUND)); + + private RecordingType(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static RecordingType from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(RecordingType e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Say.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Say.java new file mode 100644 index 000000000..64ae06224 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/Say.java @@ -0,0 +1,116 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +@JsonDeserialize(builder = SayImpl.Builder.class) +public interface Say { + + /** Gets or Sets format */ + public class FormatEnum extends EnumDynamic { + public static final FormatEnum TEXT = new FormatEnum("TEXT"); + + public static final FormatEnum SSML = new FormatEnum("SSML"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(FormatEnum.class, FormatEnum::new, Arrays.asList(TEXT, SSML)); + + private FormatEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static FormatEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(FormatEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * The text to be synthesized into speech. + * + *

If format is TEXT (default), provide plain text. If format + * is SSML, provide a valid SSML document (for example, + * <speak>...</speak>). + * + *

Field is required + * + * @return text + */ + String getText(); + + /** + * Format of the message + * + * @return format + */ + FormatEnum getFormat(); + + /** + * The name of the voice to use for text-to-speech synthesis. + * + *

Supported voices include: Emma, Brian, and others. For a complete list of available voices + * and their characteristics, see the Text-to-Speech Voices documentation. + * + *

Field is required + * + * @return voiceName + */ + String getVoiceName(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SayImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param text see getter + * @return Current builder + * @see #getText + */ + Builder setText(String text); + + /** + * see getter + * + * @param format see getter + * @return Current builder + * @see #getFormat + */ + Builder setFormat(FormatEnum format); + + /** + * see getter + * + * @param voiceName see getter + * @return Current builder + * @see #getVoiceName + */ + Builder setVoiceName(String voiceName); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + Say build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayImpl.java new file mode 100644 index 000000000..12e4b42b2 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayImpl.java @@ -0,0 +1,147 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + SayImpl.JSON_PROPERTY_TEXT, + SayImpl.JSON_PROPERTY_FORMAT, + SayImpl.JSON_PROPERTY_VOICE_NAME +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SayImpl implements Say { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TEXT = "text"; + + private OptionalValue text; + + public static final String JSON_PROPERTY_FORMAT = "format"; + + private OptionalValue format; + + public static final String JSON_PROPERTY_VOICE_NAME = "voiceName"; + + private OptionalValue voiceName; + + public SayImpl() {} + + protected SayImpl( + OptionalValue text, + OptionalValue format, + OptionalValue voiceName) { + this.text = text; + this.format = format; + this.voiceName = voiceName; + } + + @JsonIgnore + public String getText() { + return text.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TEXT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue text() { + return text; + } + + @JsonIgnore + public FormatEnum getFormat() { + return format.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_FORMAT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue format() { + return format; + } + + @JsonIgnore + public String getVoiceName() { + return voiceName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_VOICE_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue voiceName() { + return voiceName; + } + + /** Return true if this Say object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SayImpl say = (SayImpl) o; + return Objects.equals(this.text, say.text) + && Objects.equals(this.format, say.format) + && Objects.equals(this.voiceName, say.voiceName); + } + + @Override + public int hashCode() { + return Objects.hash(text, format, voiceName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SayImpl {\n"); + sb.append(" text: ").append(toIndentedString(text)).append("\n"); + sb.append(" format: ").append(toIndentedString(format)).append("\n"); + sb.append(" voiceName: ").append(toIndentedString(voiceName)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements Say.Builder { + OptionalValue text = OptionalValue.empty(); + OptionalValue format = OptionalValue.empty(); + OptionalValue voiceName = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TEXT, required = true) + public Builder setText(String text) { + this.text = OptionalValue.of(text); + return this; + } + + @JsonProperty(JSON_PROPERTY_FORMAT) + public Builder setFormat(FormatEnum format) { + this.format = OptionalValue.of(format); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_VOICE_NAME, required = true) + public Builder setVoiceName(String voiceName) { + this.voiceName = OptionalValue.of(voiceName); + return this; + } + + public Say build() { + return new SayImpl(text, format, voiceName); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessage.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessage.java new file mode 100644 index 000000000..3e23c1f38 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessage.java @@ -0,0 +1,78 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * A text-to-speech (TTS) message item. The platform synthesizes the provided text into speech and + * plays it on the call. + */ +@JsonDeserialize(builder = SayMessageImpl.Builder.class) +public interface SayMessage extends Message { + + /** The type property. Must have the value SAY. */ + public class TypeEnum extends EnumDynamic { + /** The SAY type. */ + public static final TypeEnum SAY = new TypeEnum("SAY"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(SAY)); + + private TypeEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Get say + * + *

Field is required + * + * @return say + */ + Say getSay(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SayMessageImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param say see getter + * @return Current builder + * @see #getSay + */ + Builder setSay(Say say); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + SayMessage build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessageImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessageImpl.java new file mode 100644 index 000000000..3ccc31055 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessageImpl.java @@ -0,0 +1,117 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({SayMessageImpl.JSON_PROPERTY_TYPE, SayMessageImpl.JSON_PROPERTY_SAY}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SayMessageImpl implements SayMessage, Message { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_SAY = "say"; + + private OptionalValue say; + + public SayMessageImpl() {} + + protected SayMessageImpl(OptionalValue type, OptionalValue say) { + this.type = type; + this.say = say; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public Say getSay() { + return say.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SAY) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue say() { + return say; + } + + /** Return true if this SayMessage object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SayMessageImpl sayMessage = (SayMessageImpl) o; + return Objects.equals(this.type, sayMessage.type) && Objects.equals(this.say, sayMessage.say); + } + + @Override + public int hashCode() { + return Objects.hash(type, say); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SayMessageImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" say: ").append(toIndentedString(say)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements SayMessage.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.SAY); + OptionalValue say = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.SAY)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.SAY, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SAY, required = true) + public Builder setSay(Say say) { + this.say = OptionalValue.of(say); + return this; + } + + public SayMessage build() { + return new SayMessageImpl(type, say); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommand.java new file mode 100644 index 000000000..43333d611 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommand.java @@ -0,0 +1,119 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Starts recording the call. This is a non-blocking command — execution continues to the next + * command in the sequence immediately after recording begins. + */ +@JsonDeserialize(builder = StartRecordingCommandImpl.Builder.class) +public interface StartRecordingCommand extends SvamlCommand { + + /** The command property. Must have the value startRecording. */ + public class CommandEnum extends EnumDynamic { + /** The startRecording command. */ + public static final CommandEnum START_RECORDING = new CommandEnum("startRecording"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + CommandEnum.class, CommandEnum::new, Arrays.asList(START_RECORDING)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Identifier for this recording within the session. Must be unique across active recordings in + * the session. + * + *

Other commands (e.g., stopRecording) reference this name to target a specific + * recording. + * + *

Setting the recording name is useful for stopping the recording using the + * stopRecording command. If name is not set, recording can only be stopped when the call + * is disconnected. + * + * @return recordingName + */ + String getRecordingName(); + + /** + * Recording options for this recording. + * + *

Field is required + * + * @return recordingOptions + */ + RecordingOptions getRecordingOptions(); + + /** + * SVAML commands to execute based on recording lifecycle outcomes. + * + * @return events + */ + RecordingEvents getEvents(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StartRecordingCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param recordingName see getter + * @return Current builder + * @see #getRecordingName + */ + Builder setRecordingName(String recordingName); + + /** + * see getter + * + * @param recordingOptions see getter + * @return Current builder + * @see #getRecordingOptions + */ + Builder setRecordingOptions(RecordingOptions recordingOptions); + + /** + * see getter + * + * @param events see getter + * @return Current builder + * @see #getEvents + */ + Builder setEvents(RecordingEvents events); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + StartRecordingCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommandImpl.java new file mode 100644 index 000000000..442383057 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommandImpl.java @@ -0,0 +1,178 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + StartRecordingCommandImpl.JSON_PROPERTY_COMMAND, + StartRecordingCommandImpl.JSON_PROPERTY_RECORDING_NAME, + StartRecordingCommandImpl.JSON_PROPERTY_RECORDING_OPTIONS, + StartRecordingCommandImpl.JSON_PROPERTY_EVENTS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StartRecordingCommandImpl implements StartRecordingCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_RECORDING_NAME = "recordingName"; + + private OptionalValue recordingName; + + public static final String JSON_PROPERTY_RECORDING_OPTIONS = "recordingOptions"; + + private OptionalValue recordingOptions; + + public static final String JSON_PROPERTY_EVENTS = "events"; + + private OptionalValue events; + + public StartRecordingCommandImpl() {} + + protected StartRecordingCommandImpl( + OptionalValue command, + OptionalValue recordingName, + OptionalValue recordingOptions, + OptionalValue events) { + this.command = command; + this.recordingName = recordingName; + this.recordingOptions = recordingOptions; + this.events = events; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getRecordingName() { + return recordingName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_RECORDING_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue recordingName() { + return recordingName; + } + + @JsonIgnore + public RecordingOptions getRecordingOptions() { + return recordingOptions.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_RECORDING_OPTIONS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue recordingOptions() { + return recordingOptions; + } + + @JsonIgnore + public RecordingEvents getEvents() { + return events.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue events() { + return events; + } + + /** Return true if this StartRecordingCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StartRecordingCommandImpl startRecordingCommand = (StartRecordingCommandImpl) o; + return Objects.equals(this.command, startRecordingCommand.command) + && Objects.equals(this.recordingName, startRecordingCommand.recordingName) + && Objects.equals(this.recordingOptions, startRecordingCommand.recordingOptions) + && Objects.equals(this.events, startRecordingCommand.events); + } + + @Override + public int hashCode() { + return Objects.hash(command, recordingName, recordingOptions, events); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StartRecordingCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" recordingName: ").append(toIndentedString(recordingName)).append("\n"); + sb.append(" recordingOptions: ").append(toIndentedString(recordingOptions)).append("\n"); + sb.append(" events: ").append(toIndentedString(events)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements StartRecordingCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.START_RECORDING); + OptionalValue recordingName = OptionalValue.empty(); + OptionalValue recordingOptions = OptionalValue.empty(); + OptionalValue events = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.START_RECORDING)) { + throw new IllegalArgumentException( + String.format( + "'command' must be '%s' (is '%s')", CommandEnum.START_RECORDING, command)); + } + return this; + } + + @JsonProperty(JSON_PROPERTY_RECORDING_NAME) + public Builder setRecordingName(String recordingName) { + this.recordingName = OptionalValue.of(recordingName); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_RECORDING_OPTIONS, required = true) + public Builder setRecordingOptions(RecordingOptions recordingOptions) { + this.recordingOptions = OptionalValue.of(recordingOptions); + return this; + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + public Builder setEvents(RecordingEvents events) { + this.events = OptionalValue.of(events); + return this; + } + + public StartRecordingCommand build() { + return new StartRecordingCommandImpl(command, recordingName, recordingOptions, events); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommand.java new file mode 100644 index 000000000..65b4303c4 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommand.java @@ -0,0 +1,82 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Stops a message sequence started by a messages command. It will also cancel any + * messages with specified messagesName that will appear in the call session in the + * future. + * + *

This is a non-blocking command. + */ +@JsonDeserialize(builder = StopMessagesCommandImpl.Builder.class) +public interface StopMessagesCommand extends SvamlCommand { + + /** The command property. Must have the value stopMessages. */ + public class CommandEnum extends EnumDynamic { + /** The stopMessages command. */ + public static final CommandEnum STOP_MESSAGES = new CommandEnum("stopMessages"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(CommandEnum.class, CommandEnum::new, Arrays.asList(STOP_MESSAGES)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Name of the message sequence to stop, as set by messagesName in the messages + * command. + * + *

Field is required + * + * @return messagesName + */ + String getMessagesName(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StopMessagesCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param messagesName see getter + * @return Current builder + * @see #getMessagesName + */ + Builder setMessagesName(String messagesName); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + StopMessagesCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommandImpl.java new file mode 100644 index 000000000..4f3920a4f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommandImpl.java @@ -0,0 +1,122 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + StopMessagesCommandImpl.JSON_PROPERTY_COMMAND, + StopMessagesCommandImpl.JSON_PROPERTY_MESSAGES_NAME +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StopMessagesCommandImpl implements StopMessagesCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_MESSAGES_NAME = "messagesName"; + + private OptionalValue messagesName; + + public StopMessagesCommandImpl() {} + + protected StopMessagesCommandImpl( + OptionalValue command, OptionalValue messagesName) { + this.command = command; + this.messagesName = messagesName; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getMessagesName() { + return messagesName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MESSAGES_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue messagesName() { + return messagesName; + } + + /** Return true if this StopMessagesCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StopMessagesCommandImpl stopMessagesCommand = (StopMessagesCommandImpl) o; + return Objects.equals(this.command, stopMessagesCommand.command) + && Objects.equals(this.messagesName, stopMessagesCommand.messagesName); + } + + @Override + public int hashCode() { + return Objects.hash(command, messagesName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StopMessagesCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" messagesName: ").append(toIndentedString(messagesName)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements StopMessagesCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.STOP_MESSAGES); + OptionalValue messagesName = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.STOP_MESSAGES)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.STOP_MESSAGES, command)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_MESSAGES_NAME, required = true) + public Builder setMessagesName(String messagesName) { + this.messagesName = OptionalValue.of(messagesName); + return this; + } + + public StopMessagesCommand build() { + return new StopMessagesCommandImpl(command, messagesName); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommand.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommand.java new file mode 100644 index 000000000..3bde1c5ab --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommand.java @@ -0,0 +1,81 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Stops a recording previously started by a startRecording command. This is a + * non-blocking command — execution continues to the next command in the sequence immediately after + * the stop is initiated. + */ +@JsonDeserialize(builder = StopRecordingCommandImpl.Builder.class) +public interface StopRecordingCommand extends SvamlCommand { + + /** The command property. Must have the value stopRecording. */ + public class CommandEnum extends EnumDynamic { + /** The stopRecording command. */ + public static final CommandEnum STOP_RECORDING = new CommandEnum("stopRecording"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + CommandEnum.class, CommandEnum::new, Arrays.asList(STOP_RECORDING)); + + private CommandEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CommandEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CommandEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * Name of the recording to stop, as set by recordingName in the startRecording + * command. + * + *

Field is required + * + * @return recordingName + */ + String getRecordingName(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StopRecordingCommandImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param recordingName see getter + * @return Current builder + * @see #getRecordingName + */ + Builder setRecordingName(String recordingName); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + StopRecordingCommand build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommandImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommandImpl.java new file mode 100644 index 000000000..1a5a9171a --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommandImpl.java @@ -0,0 +1,122 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + StopRecordingCommandImpl.JSON_PROPERTY_COMMAND, + StopRecordingCommandImpl.JSON_PROPERTY_RECORDING_NAME +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StopRecordingCommandImpl implements StopRecordingCommand, SvamlCommand { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMAND = "command"; + + private OptionalValue command; + + public static final String JSON_PROPERTY_RECORDING_NAME = "recordingName"; + + private OptionalValue recordingName; + + public StopRecordingCommandImpl() {} + + protected StopRecordingCommandImpl( + OptionalValue command, OptionalValue recordingName) { + this.command = command; + this.recordingName = recordingName; + } + + @JsonIgnore + public CommandEnum getCommand() { + return command.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMAND) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue command() { + return command; + } + + @JsonIgnore + public String getRecordingName() { + return recordingName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_RECORDING_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue recordingName() { + return recordingName; + } + + /** Return true if this StopRecordingCommand object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StopRecordingCommandImpl stopRecordingCommand = (StopRecordingCommandImpl) o; + return Objects.equals(this.command, stopRecordingCommand.command) + && Objects.equals(this.recordingName, stopRecordingCommand.recordingName); + } + + @Override + public int hashCode() { + return Objects.hash(command, recordingName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StopRecordingCommandImpl {\n"); + sb.append(" command: ").append(toIndentedString(command)).append("\n"); + sb.append(" recordingName: ").append(toIndentedString(recordingName)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements StopRecordingCommand.Builder { + OptionalValue command = OptionalValue.of(CommandEnum.STOP_RECORDING); + OptionalValue recordingName = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMAND, required = true) + Builder setCommand(CommandEnum command) { + if (!Objects.equals(command, CommandEnum.STOP_RECORDING)) { + throw new IllegalArgumentException( + String.format("'command' must be '%s' (is '%s')", CommandEnum.STOP_RECORDING, command)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_RECORDING_NAME, required = true) + public Builder setRecordingName(String recordingName) { + this.recordingName = OptionalValue.of(recordingName); + return this; + } + + public StopRecordingCommand build() { + return new StopRecordingCommandImpl(command, recordingName); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptions.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptions.java new file mode 100644 index 000000000..f70f2e256 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptions.java @@ -0,0 +1,62 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** Configuration for automatic speech-to-text transcription of the recording. */ +@JsonDeserialize(builder = TranscriptionOptionsImpl.Builder.class) +public interface TranscriptionOptions { + + /** + * If true, the recording will be transcribed to text. + * + *

Field is required + * + * @return isEnabled + */ + Boolean getIsEnabled(); + + /** + * Language code in BCP-47 format. + * + * @return locale + */ + String getLocale(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new TranscriptionOptionsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param isEnabled see getter + * @return Current builder + * @see #getIsEnabled + */ + Builder setIsEnabled(Boolean isEnabled); + + /** + * see getter + * + * @param locale see getter + * @return Current builder + * @see #getLocale + */ + Builder setLocale(String locale); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + TranscriptionOptions build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptionsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptionsImpl.java new file mode 100644 index 000000000..9bdde023a --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptionsImpl.java @@ -0,0 +1,119 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + TranscriptionOptionsImpl.JSON_PROPERTY_IS_ENABLED, + TranscriptionOptionsImpl.JSON_PROPERTY_LOCALE +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class TranscriptionOptionsImpl implements TranscriptionOptions { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_IS_ENABLED = "isEnabled"; + + private OptionalValue isEnabled; + + public static final String JSON_PROPERTY_LOCALE = "locale"; + + private OptionalValue locale; + + public TranscriptionOptionsImpl() {} + + protected TranscriptionOptionsImpl( + OptionalValue isEnabled, OptionalValue locale) { + this.isEnabled = isEnabled; + this.locale = locale; + } + + @JsonIgnore + public Boolean getIsEnabled() { + return isEnabled.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_IS_ENABLED) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue isEnabled() { + return isEnabled; + } + + @JsonIgnore + public String getLocale() { + return locale.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_LOCALE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue locale() { + return locale; + } + + /** Return true if this TranscriptionOptions object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TranscriptionOptionsImpl transcriptionOptions = (TranscriptionOptionsImpl) o; + return Objects.equals(this.isEnabled, transcriptionOptions.isEnabled) + && Objects.equals(this.locale, transcriptionOptions.locale); + } + + @Override + public int hashCode() { + return Objects.hash(isEnabled, locale); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class TranscriptionOptionsImpl {\n"); + sb.append(" isEnabled: ").append(toIndentedString(isEnabled)).append("\n"); + sb.append(" locale: ").append(toIndentedString(locale)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements TranscriptionOptions.Builder { + OptionalValue isEnabled = OptionalValue.empty(); + OptionalValue locale = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_IS_ENABLED, required = true) + public Builder setIsEnabled(Boolean isEnabled) { + this.isEnabled = OptionalValue.of(isEnabled); + return this; + } + + @JsonProperty(JSON_PROPERTY_LOCALE) + public Builder setLocale(String locale) { + this.locale = OptionalValue.of(locale); + return this; + } + + public TranscriptionOptions build() { + return new TranscriptionOptionsImpl(isEnabled, locale); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/MessageInternal.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/MessageInternal.java new file mode 100644 index 000000000..c6d5298c8 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/MessageInternal.java @@ -0,0 +1,6 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.internal; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(using = MessageInternalImpl.MessageInternalImplDeserializer.class) +public interface MessageInternal {} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/MessageInternalImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/MessageInternalImpl.java new file mode 100644 index 000000000..de463aad7 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/MessageInternalImpl.java @@ -0,0 +1,170 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.internal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.sinch.sdk.core.models.AbstractOpenApiSchema; +import com.sinch.sdk.core.utils.databind.JSONNavigator; +import com.sinch.sdk.domains.voice.models.v2.svaml.PlayMessageImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.SayMessageImpl; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +@JsonDeserialize(using = MessageInternalImpl.MessageInternalImplDeserializer.class) +@JsonSerialize(using = MessageInternalImpl.MessageInternalImplSerializer.class) +public class MessageInternalImpl extends AbstractOpenApiSchema implements MessageInternal { + private static final Logger log = Logger.getLogger(MessageInternalImpl.class.getName()); + + public static final class MessageInternalImplSerializer + extends StdSerializer { + private static final long serialVersionUID = 1L; + + public MessageInternalImplSerializer(Class t) { + super(t); + } + + public MessageInternalImplSerializer() { + this(null); + } + + @Override + public void serialize( + MessageInternalImpl value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } + } + + public static final class MessageInternalImplDeserializer + extends StdDeserializer { + + private static final long serialVersionUID = 1L; + + public MessageInternalImplDeserializer() { + this(MessageInternalImpl.class); + } + + public MessageInternalImplDeserializer(Class vc) { + super(vc); + } + + @Override + public MessageInternalImpl deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode tree = jp.readValueAsTree(); + JsonNode discriminator = tree.get("type"); + String discriminatorValue = null != discriminator ? discriminator.asText() : null; + Class cls = null != discriminatorValue ? mappings.get(discriminatorValue) : null; + if (null == cls) { + throw new IOException( + String.format( + "Failed deserialization for MessageInternalImpl: unknown 'type' value '%s'." + + " Possible values: %s", + discriminatorValue, mappings.keySet())); + } + Object deserialized = tree.traverse(jp.getCodec()).readValueAs(cls); + MessageInternalImpl ret = new MessageInternalImpl(); + ret.setActualInstance(deserialized); + return ret; + } + + /** Handle deserialization of the 'null' value. */ + @Override + public MessageInternalImpl getNullValue(DeserializationContext ctxt) + throws JsonMappingException { + throw new JsonMappingException(ctxt.getParser(), "MessageInternalImpl cannot be null"); + } + } + + // store a list of schema names defined in oneOf + public static final Map> schemas = new HashMap<>(); + + // discriminator value -> class + private static final Map> mappings = new HashMap<>(); + + public MessageInternalImpl() { + super("oneOf", Boolean.FALSE); + } + + static { + schemas.put("PlayMessageImpl", PlayMessageImpl.class); + schemas.put("SayMessageImpl", SayMessageImpl.class); + JSONNavigator.registerDescendants( + MessageInternalImpl.class, Collections.unmodifiableMap(schemas)); + // Initialize and register the discriminator mappings. + mappings.put("PLAY", PlayMessageImpl.class); + mappings.put("SAY", SayMessageImpl.class); + JSONNavigator.registerDiscriminator(MessageInternalImpl.class, "type", mappings); + } + + @Override + public Map> getSchemas() { + return MessageInternalImpl.schemas; + } + + /** + * Set the instance that matches the oneOf child schema, check the instance parameter is valid + * against the oneOf child schemas: PlayMessageImpl, SayMessageImpl + */ + @Override + public void setActualInstance(Object instance) { + for (Class schema : schemas.values()) { + if (JSONNavigator.isInstanceOf(schema, instance, new HashSet>())) { + super.setActualInstance(instance); + return; + } + } + throw new RuntimeException("Invalid instance type. Must be PlayMessageImpl, SayMessageImpl"); + } + + /** + * Get the actual instance, which can be the following: PlayMessageImpl, SayMessageImpl + * + * @return The actual instance + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + public static class Deserializer + extends StdDeserializer { + + public Deserializer() { + this(null); + } + + public Deserializer(Class vc) { + super(vc); + } + + @Override + public com.sinch.sdk.domains.voice.models.v2.svaml.Message deserialize( + JsonParser jp, DeserializationContext ctxt) throws IOException { + + Object deserialized = jp.readValueAs(MessageInternalImpl.class).getActualInstance(); + if (null == deserialized) { + return null; + } + if (!(deserialized instanceof com.sinch.sdk.domains.voice.models.v2.svaml.Message)) { + log.log(Level.SEVERE, "Input data does not match schema ", deserialized); + return null; + } + + return (com.sinch.sdk.domains.voice.models.v2.svaml.Message) deserialized; + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/SvamlCommandInternal.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/SvamlCommandInternal.java new file mode 100644 index 000000000..8863ad872 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/SvamlCommandInternal.java @@ -0,0 +1,6 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.internal; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(using = SvamlCommandInternalImpl.SvamlCommandInternalImplDeserializer.class) +public interface SvamlCommandInternal {} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/SvamlCommandInternalImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/SvamlCommandInternalImpl.java new file mode 100644 index 000000000..9964a91ea --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/internal/SvamlCommandInternalImpl.java @@ -0,0 +1,214 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.internal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.sinch.sdk.core.models.AbstractOpenApiSchema; +import com.sinch.sdk.core.utils.databind.JSONNavigator; +import com.sinch.sdk.domains.voice.models.v2.svaml.AmdCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.AnswerCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.BridgeCallCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.CustomEventCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.DialCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.GotoMenuCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.HangupCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.MenuCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.MessagesCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.PauseCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.StartRecordingCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.StopMessagesCommandImpl; +import com.sinch.sdk.domains.voice.models.v2.svaml.StopRecordingCommandImpl; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +@JsonDeserialize(using = SvamlCommandInternalImpl.SvamlCommandInternalImplDeserializer.class) +@JsonSerialize(using = SvamlCommandInternalImpl.SvamlCommandInternalImplSerializer.class) +public class SvamlCommandInternalImpl extends AbstractOpenApiSchema + implements SvamlCommandInternal { + private static final Logger log = Logger.getLogger(SvamlCommandInternalImpl.class.getName()); + + public static final class SvamlCommandInternalImplSerializer + extends StdSerializer { + private static final long serialVersionUID = 1L; + + public SvamlCommandInternalImplSerializer(Class t) { + super(t); + } + + public SvamlCommandInternalImplSerializer() { + this(null); + } + + @Override + public void serialize( + SvamlCommandInternalImpl value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } + } + + public static final class SvamlCommandInternalImplDeserializer + extends StdDeserializer { + + private static final long serialVersionUID = 1L; + + public SvamlCommandInternalImplDeserializer() { + this(SvamlCommandInternalImpl.class); + } + + public SvamlCommandInternalImplDeserializer(Class vc) { + super(vc); + } + + @Override + public SvamlCommandInternalImpl deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode tree = jp.readValueAsTree(); + JsonNode discriminator = tree.get("command"); + String discriminatorValue = null != discriminator ? discriminator.asText() : null; + Class cls = null != discriminatorValue ? mappings.get(discriminatorValue) : null; + if (null == cls) { + throw new IOException( + String.format( + "Failed deserialization for SvamlCommandInternalImpl: unknown 'command' value '%s'." + + " Possible values: %s", + discriminatorValue, mappings.keySet())); + } + Object deserialized = tree.traverse(jp.getCodec()).readValueAs(cls); + SvamlCommandInternalImpl ret = new SvamlCommandInternalImpl(); + ret.setActualInstance(deserialized); + return ret; + } + + /** Handle deserialization of the 'null' value. */ + @Override + public SvamlCommandInternalImpl getNullValue(DeserializationContext ctxt) + throws JsonMappingException { + throw new JsonMappingException(ctxt.getParser(), "SvamlCommandInternalImpl cannot be null"); + } + } + + // store a list of schema names defined in oneOf + public static final Map> schemas = new HashMap<>(); + + // discriminator value -> class + private static final Map> mappings = new HashMap<>(); + + public SvamlCommandInternalImpl() { + super("oneOf", Boolean.FALSE); + } + + static { + schemas.put("AmdCommandImpl", AmdCommandImpl.class); + schemas.put("AnswerCommandImpl", AnswerCommandImpl.class); + schemas.put("BridgeCallCommandImpl", BridgeCallCommandImpl.class); + schemas.put("DialCommandImpl", DialCommandImpl.class); + schemas.put("GotoMenuCommandImpl", GotoMenuCommandImpl.class); + schemas.put("HangupCommandImpl", HangupCommandImpl.class); + schemas.put("MenuCommandImpl", MenuCommandImpl.class); + schemas.put("MessagesCommandImpl", MessagesCommandImpl.class); + schemas.put("PauseCommandImpl", PauseCommandImpl.class); + schemas.put("StartRecordingCommandImpl", StartRecordingCommandImpl.class); + schemas.put("StopMessagesCommandImpl", StopMessagesCommandImpl.class); + schemas.put("StopRecordingCommandImpl", StopRecordingCommandImpl.class); + schemas.put("CustomEventCommandImpl", CustomEventCommandImpl.class); + JSONNavigator.registerDescendants( + SvamlCommandInternalImpl.class, Collections.unmodifiableMap(schemas)); + // Initialize and register the discriminator mappings. + mappings.put("amd", AmdCommandImpl.class); + mappings.put("answer", AnswerCommandImpl.class); + mappings.put("bridgeCall", BridgeCallCommandImpl.class); + mappings.put("dial", DialCommandImpl.class); + mappings.put("gotoMenu", GotoMenuCommandImpl.class); + mappings.put("hangup", HangupCommandImpl.class); + mappings.put("menu", MenuCommandImpl.class); + mappings.put("messages", MessagesCommandImpl.class); + mappings.put("pause", PauseCommandImpl.class); + mappings.put("startRecording", StartRecordingCommandImpl.class); + mappings.put("stopMessages", StopMessagesCommandImpl.class); + mappings.put("stopRecording", StopRecordingCommandImpl.class); + mappings.put("webhook", CustomEventCommandImpl.class); + JSONNavigator.registerDiscriminator(SvamlCommandInternalImpl.class, "command", mappings); + } + + @Override + public Map> getSchemas() { + return SvamlCommandInternalImpl.schemas; + } + + /** + * Set the instance that matches the oneOf child schema, check the instance parameter is valid + * against the oneOf child schemas: AmdCommandImpl, AnswerCommandImpl, BridgeCallCommandImpl, + * DialCommandImpl, GotoMenuCommandImpl, HangupCommandImpl, MenuCommandImpl, MessagesCommandImpl, + * PauseCommandImpl, StartRecordingCommandImpl, StopMessagesCommandImpl, StopRecordingCommandImpl, + * CustomEventCommandImpl + */ + @Override + public void setActualInstance(Object instance) { + for (Class schema : schemas.values()) { + if (JSONNavigator.isInstanceOf(schema, instance, new HashSet>())) { + super.setActualInstance(instance); + return; + } + } + throw new RuntimeException( + "Invalid instance type. Must be AmdCommandImpl, AnswerCommandImpl, BridgeCallCommandImpl," + + " DialCommandImpl, GotoMenuCommandImpl, HangupCommandImpl, MenuCommandImpl," + + " MessagesCommandImpl, PauseCommandImpl, StartRecordingCommandImpl," + + " StopMessagesCommandImpl, StopRecordingCommandImpl, CustomEventCommandImpl"); + } + + /** + * Get the actual instance, which can be the following: AmdCommandImpl, AnswerCommandImpl, + * BridgeCallCommandImpl, DialCommandImpl, GotoMenuCommandImpl, HangupCommandImpl, + * MenuCommandImpl, MessagesCommandImpl, PauseCommandImpl, StartRecordingCommandImpl, + * StopMessagesCommandImpl, StopRecordingCommandImpl, CustomEventCommandImpl + * + * @return The actual instance + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + public static class Deserializer + extends StdDeserializer { + + public Deserializer() { + this(null); + } + + public Deserializer(Class vc) { + super(vc); + } + + @Override + public com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand deserialize( + JsonParser jp, DeserializationContext ctxt) throws IOException { + + Object deserialized = jp.readValueAs(SvamlCommandInternalImpl.class).getActualInstance(); + if (null == deserialized) { + return null; + } + if (!(deserialized instanceof com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand)) { + log.log(Level.SEVERE, "Input data does not match schema ", deserialized); + return null; + } + + return (com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand) deserialized; + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestDtoTest.java new file mode 100644 index 000000000..5f00f22e4 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestDtoTest.java @@ -0,0 +1,32 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.request; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.DialCommandDtoTest; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StartCallRequestDtoTest extends BaseTest { + + public static final StartCallRequest expectedStartCallRequest = + StartCallRequest.builder() + .setCommands(Arrays.asList(DialCommandDtoTest.expectedDialCommand)) + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setIdempotencyKey("3f1c7a52-8d3e-4b9a-9c0e-2f6d1b7a4e21") + .build(); + + @GivenTextResource("/domains/voice/v2/calls/request/StartCallRequestDto.json") + String jsonStartCallRequest; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStartCallRequest); + + JSONAssert.assertEquals(jsonStartCallRequest, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponseDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponseDtoTest.java new file mode 100644 index 000000000..9b6bf677e --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/response/StartCallResponseDtoTest.java @@ -0,0 +1,39 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StartCallResponseDtoTest extends BaseTest { + + public static final StartCallResponse expectedStartCallResponse = + StartCallResponse.builder() + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setSessionId("01BX5ZZKBKACTAV9WEVGEMMVRB") + .build(); + + @GivenTextResource("/domains/voice/v2/calls/response/StartCallResponseDto.json") + String jsonStartCallResponse; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStartCallResponse); + + JSONAssert.assertEquals(jsonStartCallResponse, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + StartCallResponse deserialized = + objectMapper.readValue(jsonStartCallResponse, StartCallResponse.class); + + TestHelpers.recursiveEquals(deserialized, expectedStartCallResponse); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/CallHeaderDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/CallHeaderDtoTest.java new file mode 100644 index 000000000..1ee03d2d4 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/CallHeaderDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class CallHeaderDtoTest extends BaseTest { + + public static final CallHeader expectedCallHeader = + CallHeader.builder().setKey("customer-id").setValue("12345").build(); + + @GivenTextResource("/domains/voice/v2/destination/CallHeaderDto.json") + String jsonCallHeader; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedCallHeader); + + JSONAssert.assertEquals(jsonCallHeader, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + CallHeader deserialized = objectMapper.readValue(jsonCallHeader, CallHeader.class); + + TestHelpers.recursiveEquals(deserialized, expectedCallHeader); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetailsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetailsDtoTest.java new file mode 100644 index 000000000..ad1b170f3 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDetailsDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class PhoneDetailsDtoTest extends BaseTest { + + public static final PhoneDetails expectedPhoneDetails = + PhoneDetails.builder().setNumber("+15551234567").build(); + + @GivenTextResource("/domains/voice/v2/destination/PhoneDetailsDto.json") + String jsonPhoneDetails; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedPhoneDetails); + + JSONAssert.assertEquals(jsonPhoneDetails, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + PhoneDetails deserialized = objectMapper.readValue(jsonPhoneDetails, PhoneDetails.class); + + TestHelpers.recursiveEquals(deserialized, expectedPhoneDetails); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDtoTest.java new file mode 100644 index 000000000..241e55ef0 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/PhoneDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class PhoneDtoTest extends BaseTest { + + public static final Phone expectedPhone = + Phone.builder().setPhone(PhoneDetailsDtoTest.expectedPhoneDetails).build(); + + @GivenTextResource("/domains/voice/v2/destination/PhoneDto.json") + String jsonPhone; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedPhone); + + JSONAssert.assertEquals(jsonPhone, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + Phone deserialized = objectMapper.readValue(jsonPhone, Phone.class); + + TestHelpers.recursiveEquals(deserialized, expectedPhone); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeaderDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeaderDtoTest.java new file mode 100644 index 000000000..1ab1a4ef3 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipCallHeaderDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SipCallHeaderDtoTest extends BaseTest { + + public static final SipCallHeader expectedSipCallHeader = + SipCallHeader.builder().setKey("X-Custom-Header").setValue("custom-value").build(); + + @GivenTextResource("/domains/voice/v2/destination/SipCallHeaderDto.json") + String jsonSipCallHeader; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSipCallHeader); + + JSONAssert.assertEquals(jsonSipCallHeader, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + SipCallHeader deserialized = objectMapper.readValue(jsonSipCallHeader, SipCallHeader.class); + + TestHelpers.recursiveEquals(deserialized, expectedSipCallHeader); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipDetailsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipDetailsDtoTest.java new file mode 100644 index 000000000..54ec98222 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipDetailsDtoTest.java @@ -0,0 +1,39 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SipDetailsDtoTest extends BaseTest { + + public static final SipDetails expectedSipDetails = + SipDetails.builder() + .setEndpoint("sips:bob@sip.example.com") + .setTransport(SipDetails.TransportEnum.TLS) + .setCallHeaders(Arrays.asList(SipCallHeaderDtoTest.expectedSipCallHeader)) + .build(); + + @GivenTextResource("/domains/voice/v2/destination/SipDetailsDto.json") + String jsonSipDetails; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSipDetails); + + JSONAssert.assertEquals(jsonSipDetails, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + SipDetails deserialized = objectMapper.readValue(jsonSipDetails, SipDetails.class); + + TestHelpers.recursiveEquals(deserialized, expectedSipDetails); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipDtoTest.java new file mode 100644 index 000000000..4949c7e1f --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SipDtoTest extends BaseTest { + + public static final Sip expectedSip = + Sip.builder().setSip(SipDetailsDtoTest.expectedSipDetails).build(); + + @GivenTextResource("/domains/voice/v2/destination/SipDto.json") + String jsonSip; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSip); + + JSONAssert.assertEquals(jsonSip, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + Sip deserialized = objectMapper.readValue(jsonSip, Sip.class); + + TestHelpers.recursiveEquals(deserialized, expectedSip); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetailsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetailsDtoTest.java new file mode 100644 index 000000000..909dc1843 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDetailsDtoTest.java @@ -0,0 +1,37 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SipFromDetailsDtoTest extends BaseTest { + + public static final SipFromDetails expectedSipFromDetails = + SipFromDetails.builder() + .setEndpoint("sip:alice@sip.example.com") + .setDisplayName("Alice") + .build(); + + @GivenTextResource("/domains/voice/v2/destination/SipFromDetailsDto.json") + String jsonSipFromDetails; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSipFromDetails); + + JSONAssert.assertEquals(jsonSipFromDetails, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + SipFromDetails deserialized = objectMapper.readValue(jsonSipFromDetails, SipFromDetails.class); + + TestHelpers.recursiveEquals(deserialized, expectedSipFromDetails); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDtoTest.java new file mode 100644 index 000000000..60cd88e81 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/SipFromDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SipFromDtoTest extends BaseTest { + + public static final SipFrom expectedSipFrom = + SipFrom.builder().setSip(SipFromDetailsDtoTest.expectedSipFromDetails).build(); + + @GivenTextResource("/domains/voice/v2/destination/SipFromDto.json") + String jsonSipFrom; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSipFrom); + + JSONAssert.assertEquals(jsonSipFrom, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + SipFrom deserialized = objectMapper.readValue(jsonSipFrom, SipFrom.class); + + TestHelpers.recursiveEquals(deserialized, expectedSipFrom); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetailsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetailsDtoTest.java new file mode 100644 index 000000000..354ff3407 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamDetailsDtoTest.java @@ -0,0 +1,39 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StreamDetailsDtoTest extends BaseTest { + + public static final StreamDetails expectedStreamDetails = + StreamDetails.builder() + .setEndpoint("wss://myapp.example.com/audio") + .setStreamOptions(StreamOptionsDtoTest.expectedStreamOptions) + .setCallHeaders(Arrays.asList(CallHeaderDtoTest.expectedCallHeader)) + .build(); + + @GivenTextResource("/domains/voice/v2/destination/StreamDetailsDto.json") + String jsonStreamDetails; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStreamDetails); + + JSONAssert.assertEquals(jsonStreamDetails, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + StreamDetails deserialized = objectMapper.readValue(jsonStreamDetails, StreamDetails.class); + + TestHelpers.recursiveEquals(deserialized, expectedStreamDetails); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamDtoTest.java new file mode 100644 index 000000000..19fd695a6 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StreamDtoTest extends BaseTest { + + public static final Stream expectedStream = + Stream.builder().setStream(StreamDetailsDtoTest.expectedStreamDetails).build(); + + @GivenTextResource("/domains/voice/v2/destination/StreamDto.json") + String jsonStream; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStream); + + JSONAssert.assertEquals(jsonStream, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + Stream deserialized = objectMapper.readValue(jsonStream, Stream.class); + + TestHelpers.recursiveEquals(deserialized, expectedStream); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptionsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptionsDtoTest.java new file mode 100644 index 000000000..1c0f62c18 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/StreamOptionsDtoTest.java @@ -0,0 +1,38 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StreamOptionsDtoTest extends BaseTest { + + public static final StreamOptions expectedStreamOptions = + StreamOptions.builder() + .setVersion(1) + .setCodec(StreamOptions.CodecEnum.PCM) + .setSampleRate(StreamOptions.SampleRateEnum.NUMBER_16000) + .build(); + + @GivenTextResource("/domains/voice/v2/destination/StreamOptionsDto.json") + String jsonStreamOptions; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStreamOptions); + + JSONAssert.assertEquals(jsonStreamOptions, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + StreamOptions deserialized = objectMapper.readValue(jsonStreamOptions, StreamOptions.class); + + TestHelpers.recursiveEquals(deserialized, expectedStreamOptions); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetailsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetailsDtoTest.java new file mode 100644 index 000000000..5338e8042 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDetailsDtoTest.java @@ -0,0 +1,42 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class VoiceRelayDetailsDtoTest extends BaseTest { + + public static final VoiceRelayDetails expectedVoiceRelayDetails = + VoiceRelayDetails.builder() + .setEndpoint("wss://relay.example.com/agent") + .setEnableInterruptions(false) + .setTtsVoice("Emma") + .setSttLanguage("en-US") + .setCallHeaders(Arrays.asList(CallHeaderDtoTest.expectedCallHeader)) + .build(); + + @GivenTextResource("/domains/voice/v2/destination/VoiceRelayDetailsDto.json") + String jsonVoiceRelayDetails; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedVoiceRelayDetails); + + JSONAssert.assertEquals(jsonVoiceRelayDetails, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + VoiceRelayDetails deserialized = + objectMapper.readValue(jsonVoiceRelayDetails, VoiceRelayDetails.class); + + TestHelpers.recursiveEquals(deserialized, expectedVoiceRelayDetails); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDtoTest.java new file mode 100644 index 000000000..6656ce054 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/destination/VoiceRelayDtoTest.java @@ -0,0 +1,36 @@ +package com.sinch.sdk.domains.voice.models.v2.destination; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class VoiceRelayDtoTest extends BaseTest { + + public static final VoiceRelay expectedVoiceRelay = + VoiceRelay.builder() + .setVoiceRelay(VoiceRelayDetailsDtoTest.expectedVoiceRelayDetails) + .build(); + + @GivenTextResource("/domains/voice/v2/destination/VoiceRelayDto.json") + String jsonVoiceRelay; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedVoiceRelay); + + JSONAssert.assertEquals(jsonVoiceRelay, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + VoiceRelay deserialized = objectMapper.readValue(jsonVoiceRelay, VoiceRelay.class); + + TestHelpers.recursiveEquals(deserialized, expectedVoiceRelay); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommandDtoTest.java new file mode 100644 index 000000000..1c7f438b1 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AmdCommandDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class AmdCommandDtoTest extends BaseTest { + + public static final AmdCommand expectedAmdCommand = + AmdCommand.builder().setEvents(AmdEventsDtoTest.expectedAmdEvents).build(); + + @GivenTextResource("/domains/voice/v2/svaml/AmdCommandDto.json") + String jsonAmdCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedAmdCommand); + + JSONAssert.assertEquals(jsonAmdCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + AmdCommand deserialized = objectMapper.readValue(jsonAmdCommand, AmdCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedAmdCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEventsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEventsDtoTest.java new file mode 100644 index 000000000..713cd4e38 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AmdEventsDtoTest.java @@ -0,0 +1,40 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class AmdEventsDtoTest extends BaseTest { + + public static final AmdEvents expectedAmdEvents = + AmdEvents.builder() + .setOnHuman(Arrays.asList(MessagesCommandDtoTest.expectedMessagesCommand)) + .setOnMachine(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .setOnBeep(Arrays.asList(PauseCommandDtoTest.expectedPauseCommand)) + .setOnUnknown(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/AmdEventsDto.json") + String jsonAmdEvents; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedAmdEvents); + + JSONAssert.assertEquals(jsonAmdEvents, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + AmdEvents deserialized = objectMapper.readValue(jsonAmdEvents, AmdEvents.class); + + TestHelpers.recursiveEquals(deserialized, expectedAmdEvents); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommandDtoTest.java new file mode 100644 index 000000000..3abe4c769 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/AnswerCommandDtoTest.java @@ -0,0 +1,33 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class AnswerCommandDtoTest extends BaseTest { + + public static final AnswerCommand expectedAnswerCommand = AnswerCommand.builder().build(); + + @GivenTextResource("/domains/voice/v2/svaml/AnswerCommandDto.json") + String jsonAnswerCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedAnswerCommand); + + JSONAssert.assertEquals(jsonAnswerCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + AnswerCommand deserialized = objectMapper.readValue(jsonAnswerCommand, AnswerCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedAnswerCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommandDtoTest.java new file mode 100644 index 000000000..cc1d2bc4c --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/BridgeCallCommandDtoTest.java @@ -0,0 +1,35 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class BridgeCallCommandDtoTest extends BaseTest { + + public static final BridgeCallCommand expectedBridgeCallCommand = + BridgeCallCommand.builder().setBridgeName("bridge").build(); + + @GivenTextResource("/domains/voice/v2/svaml/BridgeCallCommandDto.json") + String jsonBridgeCallCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedBridgeCallCommand); + + JSONAssert.assertEquals(jsonBridgeCallCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + BridgeCallCommand deserialized = + objectMapper.readValue(jsonBridgeCallCommand, BridgeCallCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedBridgeCallCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/CallEventsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/CallEventsDtoTest.java new file mode 100644 index 000000000..ff0317f22 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/CallEventsDtoTest.java @@ -0,0 +1,53 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class CallEventsDtoTest extends BaseTest { + + public static final CallEvents expectedCallEvents = + CallEvents.builder() + .setOnAnswer( + Arrays.asList( + AnswerCommandDtoTest.expectedAnswerCommand, + AmdCommandDtoTest.expectedAmdCommand, + StartRecordingCommandDtoTest.expectedStartRecordingCommand, + MenuCommandDtoTest.expectedMenuCommand, + BridgeCallCommandDtoTest.expectedBridgeCallCommand, + CustomEventCommandDtoTest.expectedCustomEventCommand, + StopMessagesCommandDtoTest.expectedStopMessagesCommand)) + .setOnBusy(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .setOnReject(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .setOnTimeout(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .setOnHangup( + Arrays.asList( + StopRecordingCommandDtoTest.expectedStopRecordingCommand, + HangupCommandDtoTest.expectedHangupCommand)) + .setOnFailure(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/CallEventsDto.json") + String jsonCallEvents; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedCallEvents); + + JSONAssert.assertEquals(jsonCallEvents, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + CallEvents deserialized = objectMapper.readValue(jsonCallEvents, CallEvents.class); + + TestHelpers.recursiveEquals(deserialized, expectedCallEvents); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommandDtoTest.java new file mode 100644 index 000000000..8c9356d91 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/CustomEventCommandDtoTest.java @@ -0,0 +1,39 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class CustomEventCommandDtoTest extends BaseTest { + + public static final CustomEventCommand expectedCustomEventCommand = + CustomEventCommand.builder() + .setCustomEventName("my.custom.event") + .setUrl("https://example.com/events") + .setFallbackUrl("https://fallback.example.com/events") + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/CustomEventCommandDto.json") + String jsonCustomEventCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedCustomEventCommand); + + JSONAssert.assertEquals(jsonCustomEventCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + CustomEventCommand deserialized = + objectMapper.readValue(jsonCustomEventCommand, CustomEventCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedCustomEventCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommandDtoTest.java new file mode 100644 index 000000000..db021415e --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/DialCommandDtoTest.java @@ -0,0 +1,110 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.destination.PhoneDtoTest; +import com.sinch.sdk.domains.voice.models.v2.destination.SipDtoTest; +import com.sinch.sdk.domains.voice.models.v2.destination.SipFromDtoTest; +import com.sinch.sdk.domains.voice.models.v2.destination.StreamDtoTest; +import com.sinch.sdk.domains.voice.models.v2.destination.VoiceRelayDtoTest; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class DialCommandDtoTest extends BaseTest { + + public static final DialCommand expectedDialCommand = + DialCommand.builder() + .setCallName("origin") + .setFrom(PhoneDtoTest.expectedPhone) + .setTo(PhoneDtoTest.expectedPhone) + .setDialTimeoutDurationSeconds(30) + .setMaxCallDurationSeconds(3600) + .setEvents(CallEventsDtoTest.expectedCallEvents) + .build(); + + public static final DialCommand expectedDialCommandSip = + DialCommand.builder() + .setCallName("origin") + .setFrom(SipFromDtoTest.expectedSipFrom) + .setTo(SipDtoTest.expectedSip) + .build(); + + public static final DialCommand expectedDialCommandStream = + DialCommand.builder().setTo(StreamDtoTest.expectedStream).build(); + + public static final DialCommand expectedDialCommandVoiceRelay = + DialCommand.builder().setTo(VoiceRelayDtoTest.expectedVoiceRelay).build(); + + @GivenTextResource("/domains/voice/v2/svaml/DialCommandDto.json") + String jsonDialCommand; + + @GivenTextResource("/domains/voice/v2/svaml/DialCommandSipDto.json") + String jsonDialCommandSip; + + @GivenTextResource("/domains/voice/v2/svaml/DialCommandStreamDto.json") + String jsonDialCommandStream; + + @GivenTextResource("/domains/voice/v2/svaml/DialCommandVoiceRelayDto.json") + String jsonDialCommandVoiceRelay; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedDialCommand); + + JSONAssert.assertEquals(jsonDialCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + DialCommand deserialized = objectMapper.readValue(jsonDialCommand, DialCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedDialCommand); + } + + @Test + void serializeSip() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedDialCommandSip); + + JSONAssert.assertEquals(jsonDialCommandSip, serializedString, true); + } + + @Test + void deserializeSip() throws JsonProcessingException { + DialCommand deserialized = objectMapper.readValue(jsonDialCommandSip, DialCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedDialCommandSip); + } + + @Test + void serializeStream() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedDialCommandStream); + + JSONAssert.assertEquals(jsonDialCommandStream, serializedString, true); + } + + @Test + void deserializeStream() throws JsonProcessingException { + DialCommand deserialized = objectMapper.readValue(jsonDialCommandStream, DialCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedDialCommandStream); + } + + @Test + void serializeVoiceRelay() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedDialCommandVoiceRelay); + + JSONAssert.assertEquals(jsonDialCommandVoiceRelay, serializedString, true); + } + + @Test + void deserializeVoiceRelay() throws JsonProcessingException { + DialCommand deserialized = objectMapper.readValue(jsonDialCommandVoiceRelay, DialCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedDialCommandVoiceRelay); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommandDtoTest.java new file mode 100644 index 000000000..ecd751da6 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/GotoMenuCommandDtoTest.java @@ -0,0 +1,35 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class GotoMenuCommandDtoTest extends BaseTest { + + public static final GotoMenuCommand expectedGotoMenuCommand = + GotoMenuCommand.builder().setMenuName("main").build(); + + @GivenTextResource("/domains/voice/v2/svaml/GotoMenuCommandDto.json") + String jsonGotoMenuCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedGotoMenuCommand); + + JSONAssert.assertEquals(jsonGotoMenuCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + GotoMenuCommand deserialized = + objectMapper.readValue(jsonGotoMenuCommand, GotoMenuCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedGotoMenuCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommandDtoTest.java new file mode 100644 index 000000000..28b1cb186 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/HangupCommandDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class HangupCommandDtoTest extends BaseTest { + + public static final HangupCommand expectedHangupCommand = + HangupCommand.builder().setCallName("origin").build(); + + @GivenTextResource("/domains/voice/v2/svaml/HangupCommandDto.json") + String jsonHangupCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedHangupCommand); + + JSONAssert.assertEquals(jsonHangupCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + HangupCommand deserialized = objectMapper.readValue(jsonHangupCommand, HangupCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedHangupCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommandDtoTest.java new file mode 100644 index 000000000..149964140 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuCommandDtoTest.java @@ -0,0 +1,38 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Collections; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class MenuCommandDtoTest extends BaseTest { + + public static final MenuCommand expectedMenuCommand = + MenuCommand.builder() + .setStartMenu("main") + .setMenus(Collections.singletonMap("main", MenuItemDtoTest.expectedMenuItem)) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/MenuCommandDto.json") + String jsonMenuCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedMenuCommand); + + JSONAssert.assertEquals(jsonMenuCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + MenuCommand deserialized = objectMapper.readValue(jsonMenuCommand, MenuCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedMenuCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItemDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItemDtoTest.java new file mode 100644 index 000000000..a2e55eec0 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuItemDtoTest.java @@ -0,0 +1,49 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import java.util.Collections; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class MenuItemDtoTest extends BaseTest { + + public static final MenuItem expectedMenuItem = + MenuItem.builder() + .setPrompt(MenuPromptDtoTest.expectedMenuPrompt) + .setRepeatPrompt(MenuPromptDtoTest.expectedMenuPrompt) + .setInputTimeoutDurationSeconds(10) + .setRepeatCount(3) + .setMinimumInputLength(1) + .setMaximumInputLength(4) + .setTerminatingSequence("#") + .setInputMethods(Arrays.asList(MenuItem.InputMethodsEnum.DTMF)) + .setMatches( + Collections.singletonMap( + "1", Arrays.asList(GotoMenuCommandDtoTest.expectedGotoMenuCommand))) + .setOnFail(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/MenuItemDto.json") + String jsonMenuItem; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedMenuItem); + + JSONAssert.assertEquals(jsonMenuItem, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + MenuItem deserialized = objectMapper.readValue(jsonMenuItem, MenuItem.class); + + TestHelpers.recursiveEquals(deserialized, expectedMenuItem); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPromptDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPromptDtoTest.java new file mode 100644 index 000000000..8104bd12d --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MenuPromptDtoTest.java @@ -0,0 +1,38 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class MenuPromptDtoTest extends BaseTest { + + public static final MenuPrompt expectedMenuPrompt = + MenuPrompt.builder() + .setAllowBargeIn(false) + .setMessages(Arrays.asList(SayMessageDtoTest.expectedSayMessage)) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/MenuPromptDto.json") + String jsonMenuPrompt; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedMenuPrompt); + + JSONAssert.assertEquals(jsonMenuPrompt, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + MenuPrompt deserialized = objectMapper.readValue(jsonMenuPrompt, MenuPrompt.class); + + TestHelpers.recursiveEquals(deserialized, expectedMenuPrompt); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEventsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEventsDtoTest.java new file mode 100644 index 000000000..a7cb19774 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MessageEventsDtoTest.java @@ -0,0 +1,37 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class MessageEventsDtoTest extends BaseTest { + + public static final MessageEvents expectedMessageEvents = + MessageEvents.builder() + .setOnFinish(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/MessageEventsDto.json") + String jsonMessageEvents; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedMessageEvents); + + JSONAssert.assertEquals(jsonMessageEvents, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + MessageEvents deserialized = objectMapper.readValue(jsonMessageEvents, MessageEvents.class); + + TestHelpers.recursiveEquals(deserialized, expectedMessageEvents); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommandDtoTest.java new file mode 100644 index 000000000..c762b7b95 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/MessagesCommandDtoTest.java @@ -0,0 +1,42 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class MessagesCommandDtoTest extends BaseTest { + + public static final MessagesCommand expectedMessagesCommand = + MessagesCommand.builder() + .setMessagesName("my-messages") + .setMessages( + Arrays.asList( + SayMessageDtoTest.expectedSayMessage, PlayMessageDtoTest.expectedPlayMessage)) + .setEvents(MessageEventsDtoTest.expectedMessageEvents) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/MessagesCommandDto.json") + String jsonMessagesCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedMessagesCommand); + + JSONAssert.assertEquals(jsonMessagesCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + MessagesCommand deserialized = + objectMapper.readValue(jsonMessagesCommand, MessagesCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedMessagesCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommandDtoTest.java new file mode 100644 index 000000000..ad6849880 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PauseCommandDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class PauseCommandDtoTest extends BaseTest { + + public static final PauseCommand expectedPauseCommand = + PauseCommand.builder().setDurationMilliseconds(1500).build(); + + @GivenTextResource("/domains/voice/v2/svaml/PauseCommandDto.json") + String jsonPauseCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedPauseCommand); + + JSONAssert.assertEquals(jsonPauseCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + PauseCommand deserialized = objectMapper.readValue(jsonPauseCommand, PauseCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedPauseCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PlayDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PlayDtoTest.java new file mode 100644 index 000000000..1786c0a6e --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PlayDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class PlayDtoTest extends BaseTest { + + public static final Play expectedPlay = + Play.builder().setUrl("https://example.com/audio/welcome.mp3").build(); + + @GivenTextResource("/domains/voice/v2/svaml/PlayDto.json") + String jsonPlay; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedPlay); + + JSONAssert.assertEquals(jsonPlay, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + Play deserialized = objectMapper.readValue(jsonPlay, Play.class); + + TestHelpers.recursiveEquals(deserialized, expectedPlay); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessageDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessageDtoTest.java new file mode 100644 index 000000000..7ec3129a2 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/PlayMessageDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class PlayMessageDtoTest extends BaseTest { + + public static final PlayMessage expectedPlayMessage = + PlayMessage.builder().setPlay(PlayDtoTest.expectedPlay).build(); + + @GivenTextResource("/domains/voice/v2/svaml/PlayMessageDto.json") + String jsonPlayMessage; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedPlayMessage); + + JSONAssert.assertEquals(jsonPlayMessage, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + PlayMessage deserialized = objectMapper.readValue(jsonPlayMessage, PlayMessage.class); + + TestHelpers.recursiveEquals(deserialized, expectedPlayMessage); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEventsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEventsDtoTest.java new file mode 100644 index 000000000..8923b8496 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingEventsDtoTest.java @@ -0,0 +1,39 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class RecordingEventsDtoTest extends BaseTest { + + public static final RecordingEvents expectedRecordingEvents = + RecordingEvents.builder() + .setOnFinish(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .setOnFailure(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/RecordingEventsDto.json") + String jsonRecordingEvents; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedRecordingEvents); + + JSONAssert.assertEquals(jsonRecordingEvents, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + RecordingEvents deserialized = + objectMapper.readValue(jsonRecordingEvents, RecordingEvents.class); + + TestHelpers.recursiveEquals(deserialized, expectedRecordingEvents); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptionsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptionsDtoTest.java new file mode 100644 index 000000000..2fc069b4e --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/RecordingOptionsDtoTest.java @@ -0,0 +1,42 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class RecordingOptionsDtoTest extends BaseTest { + + public static final RecordingOptions expectedRecordingOptions = + RecordingOptions.builder() + .setFormat(RecordingFormatType.MP3) + .setRecordingType(RecordingType.COMBINED) + .setDestination(RecordingDestinationType.AWS) + .setDestinationUrl("s3://my-bucket/recordings") + .setCredentials("access-key:secret-key") + .setTranscriptionOptions(TranscriptionOptionsDtoTest.expectedTranscriptionOptions) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/RecordingOptionsDto.json") + String jsonRecordingOptions; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedRecordingOptions); + + JSONAssert.assertEquals(jsonRecordingOptions, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + RecordingOptions deserialized = + objectMapper.readValue(jsonRecordingOptions, RecordingOptions.class); + + TestHelpers.recursiveEquals(deserialized, expectedRecordingOptions); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SayDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SayDtoTest.java new file mode 100644 index 000000000..80a5b8fdb --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SayDtoTest.java @@ -0,0 +1,38 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SayDtoTest extends BaseTest { + + public static final Say expectedSay = + Say.builder() + .setText("Hello, your call is now connected.") + .setFormat(Say.FormatEnum.TEXT) + .setVoiceName("Emma") + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/SayDto.json") + String jsonSay; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSay); + + JSONAssert.assertEquals(jsonSay, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + Say deserialized = objectMapper.readValue(jsonSay, Say.class); + + TestHelpers.recursiveEquals(deserialized, expectedSay); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessageDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessageDtoTest.java new file mode 100644 index 000000000..eaaeedc10 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SayMessageDtoTest.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SayMessageDtoTest extends BaseTest { + + public static final SayMessage expectedSayMessage = + SayMessage.builder().setSay(SayDtoTest.expectedSay).build(); + + @GivenTextResource("/domains/voice/v2/svaml/SayMessageDto.json") + String jsonSayMessage; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSayMessage); + + JSONAssert.assertEquals(jsonSayMessage, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + SayMessage deserialized = objectMapper.readValue(jsonSayMessage, SayMessage.class); + + TestHelpers.recursiveEquals(deserialized, expectedSayMessage); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommandDtoTest.java new file mode 100644 index 000000000..36fa5e289 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StartRecordingCommandDtoTest.java @@ -0,0 +1,39 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StartRecordingCommandDtoTest extends BaseTest { + + public static final StartRecordingCommand expectedStartRecordingCommand = + StartRecordingCommand.builder() + .setRecordingName("my-recording") + .setRecordingOptions(RecordingOptionsDtoTest.expectedRecordingOptions) + .setEvents(RecordingEventsDtoTest.expectedRecordingEvents) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/StartRecordingCommandDto.json") + String jsonStartRecordingCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStartRecordingCommand); + + JSONAssert.assertEquals(jsonStartRecordingCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + StartRecordingCommand deserialized = + objectMapper.readValue(jsonStartRecordingCommand, StartRecordingCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedStartRecordingCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommandDtoTest.java new file mode 100644 index 000000000..2d94a2eec --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StopMessagesCommandDtoTest.java @@ -0,0 +1,35 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StopMessagesCommandDtoTest extends BaseTest { + + public static final StopMessagesCommand expectedStopMessagesCommand = + StopMessagesCommand.builder().setMessagesName("my-messages").build(); + + @GivenTextResource("/domains/voice/v2/svaml/StopMessagesCommandDto.json") + String jsonStopMessagesCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStopMessagesCommand); + + JSONAssert.assertEquals(jsonStopMessagesCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + StopMessagesCommand deserialized = + objectMapper.readValue(jsonStopMessagesCommand, StopMessagesCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedStopMessagesCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommandDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommandDtoTest.java new file mode 100644 index 000000000..783c31396 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/StopRecordingCommandDtoTest.java @@ -0,0 +1,35 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StopRecordingCommandDtoTest extends BaseTest { + + public static final StopRecordingCommand expectedStopRecordingCommand = + StopRecordingCommand.builder().setRecordingName("my-recording").build(); + + @GivenTextResource("/domains/voice/v2/svaml/StopRecordingCommandDto.json") + String jsonStopRecordingCommand; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStopRecordingCommand); + + JSONAssert.assertEquals(jsonStopRecordingCommand, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + StopRecordingCommand deserialized = + objectMapper.readValue(jsonStopRecordingCommand, StopRecordingCommand.class); + + TestHelpers.recursiveEquals(deserialized, expectedStopRecordingCommand); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlCommandTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlCommandTest.java new file mode 100644 index 000000000..80c50f736 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlCommandTest.java @@ -0,0 +1,29 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sinch.sdk.BaseTest; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +public class SvamlCommandTest extends BaseTest { + + @Test + void deserializeUnknownCommandFails() { + IOException exception = + assertThrows( + IOException.class, + () -> objectMapper.readValue("{\"command\":\"unknown\"}", SvamlCommand.class)); + assertTrue(exception.getMessage().contains("unknown 'command' value 'unknown'")); + } + + @Test + void deserializeMissingCommandFails() { + IOException exception = + assertThrows( + IOException.class, + () -> objectMapper.readValue("{\"callName\":\"origin\"}", SvamlCommand.class)); + assertTrue(exception.getMessage().contains("unknown 'command' value 'null'")); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptionsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptionsDtoTest.java new file mode 100644 index 000000000..6ff347353 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/TranscriptionOptionsDtoTest.java @@ -0,0 +1,35 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class TranscriptionOptionsDtoTest extends BaseTest { + + public static final TranscriptionOptions expectedTranscriptionOptions = + TranscriptionOptions.builder().setIsEnabled(true).setLocale("en-US").build(); + + @GivenTextResource("/domains/voice/v2/svaml/TranscriptionOptionsDto.json") + String jsonTranscriptionOptions; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedTranscriptionOptions); + + JSONAssert.assertEquals(jsonTranscriptionOptions, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + TranscriptionOptions deserialized = + objectMapper.readValue(jsonTranscriptionOptions, TranscriptionOptions.class); + + TestHelpers.recursiveEquals(deserialized, expectedTranscriptionOptions); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/StartCallRequestDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/StartCallRequestDto.json new file mode 100644 index 000000000..8f590b4bf --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/StartCallRequestDto.json @@ -0,0 +1,215 @@ +{ + "commands": [ + { + "command": "dial", + "callName": "origin", + "from": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "to": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "dialTimeoutDurationSeconds": 30, + "maxCallDurationSeconds": 3600, + "events": { + "onAnswer": [ + { + "command": "answer" + }, + { + "command": "amd", + "events": { + "onHuman": [ + { + "command": "messages", + "messagesName": "my-messages", + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + }, + { + "type": "PLAY", + "play": { + "url": "https://example.com/audio/welcome.mp3" + } + } + ], + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + ], + "onMachine": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onBeep": [ + { + "command": "pause", + "durationMilliseconds": 1500 + } + ], + "onUnknown": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + }, + { + "command": "startRecording", + "recordingName": "my-recording", + "recordingOptions": { + "format": "MP3", + "recordingType": "COMBINED", + "destination": "AWS", + "destinationUrl": "s3://my-bucket/recordings", + "credentials": "access-key:secret-key", + "transcriptionOptions": { + "isEnabled": true, + "locale": "en-US" + } + }, + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onFailure": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + }, + { + "command": "menu", + "startMenu": "main", + "menus": { + "main": { + "prompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "repeatPrompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "inputTimeoutDurationSeconds": 10, + "repeatCount": 3, + "minimumInputLength": 1, + "maximumInputLength": 4, + "terminatingSequence": "#", + "inputMethods": [ + "DTMF" + ], + "matches": { + "1": [ + { + "command": "gotoMenu", + "menuName": "main" + } + ] + }, + "onFail": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + }, + { + "command": "bridgeCall", + "bridgeName": "bridge" + }, + { + "command": "webhook", + "webhookName": "my.custom.event", + "url": "https://example.com/events", + "fallbackUrl": "https://fallback.example.com/events" + }, + { + "command": "stopMessages", + "messagesName": "my-messages" + } + ], + "onBusy": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onReject": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onTimeout": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onHangup": [ + { + "command": "stopRecording", + "recordingName": "my-recording" + }, + { + "command": "hangup", + "callName": "origin" + } + ], + "onFailure": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/StartCallResponseDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/StartCallResponseDto.json new file mode 100644 index 000000000..85a5be881 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/StartCallResponseDto.json @@ -0,0 +1,5 @@ +{ + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/CallHeaderDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/CallHeaderDto.json new file mode 100644 index 000000000..40fb7109b --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/CallHeaderDto.json @@ -0,0 +1,4 @@ +{ + "key": "customer-id", + "value": "12345" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/PhoneDetailsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/PhoneDetailsDto.json new file mode 100644 index 000000000..b190f89ba --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/PhoneDetailsDto.json @@ -0,0 +1,3 @@ +{ + "number": "+15551234567" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/PhoneDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/PhoneDto.json new file mode 100644 index 000000000..5ed464e5e --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/PhoneDto.json @@ -0,0 +1,6 @@ +{ + "type": "PHONE", + "phone": { + "number": "+15551234567" + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipCallHeaderDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipCallHeaderDto.json new file mode 100644 index 000000000..fe23a5f96 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipCallHeaderDto.json @@ -0,0 +1,4 @@ +{ + "key": "X-Custom-Header", + "value": "custom-value" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipDetailsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipDetailsDto.json new file mode 100644 index 000000000..2a402655b --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipDetailsDto.json @@ -0,0 +1,10 @@ +{ + "endpoint": "sips:bob@sip.example.com", + "transport": "TLS", + "callHeaders": [ + { + "key": "X-Custom-Header", + "value": "custom-value" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipDto.json new file mode 100644 index 000000000..820c20220 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipDto.json @@ -0,0 +1,13 @@ +{ + "type": "SIP", + "sip": { + "endpoint": "sips:bob@sip.example.com", + "transport": "TLS", + "callHeaders": [ + { + "key": "X-Custom-Header", + "value": "custom-value" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipFromDetailsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipFromDetailsDto.json new file mode 100644 index 000000000..dc3e3f286 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipFromDetailsDto.json @@ -0,0 +1,4 @@ +{ + "endpoint": "sip:alice@sip.example.com", + "displayName": "Alice" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipFromDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipFromDto.json new file mode 100644 index 000000000..abd9de4e2 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/SipFromDto.json @@ -0,0 +1,7 @@ +{ + "type": "SIP", + "sip": { + "endpoint": "sip:alice@sip.example.com", + "displayName": "Alice" + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamDetailsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamDetailsDto.json new file mode 100644 index 000000000..5bc3da5c9 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamDetailsDto.json @@ -0,0 +1,14 @@ +{ + "endpoint": "wss://myapp.example.com/audio", + "streamOptions": { + "version": 1, + "codec": "PCM", + "sampleRate": 16000 + }, + "callHeaders": [ + { + "key": "customer-id", + "value": "12345" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamDto.json new file mode 100644 index 000000000..337e308ae --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamDto.json @@ -0,0 +1,17 @@ +{ + "type": "STREAM", + "stream": { + "endpoint": "wss://myapp.example.com/audio", + "streamOptions": { + "version": 1, + "codec": "PCM", + "sampleRate": 16000 + }, + "callHeaders": [ + { + "key": "customer-id", + "value": "12345" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamOptionsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamOptionsDto.json new file mode 100644 index 000000000..03e091154 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/StreamOptionsDto.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "codec": "PCM", + "sampleRate": 16000 +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/VoiceRelayDetailsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/VoiceRelayDetailsDto.json new file mode 100644 index 000000000..0e7a56961 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/VoiceRelayDetailsDto.json @@ -0,0 +1,12 @@ +{ + "endpoint": "wss://relay.example.com/agent", + "enableInterruptions": false, + "ttsVoice": "Emma", + "sttLanguage": "en-US", + "callHeaders": [ + { + "key": "customer-id", + "value": "12345" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/destination/VoiceRelayDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/destination/VoiceRelayDto.json new file mode 100644 index 000000000..efe3045fb --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/destination/VoiceRelayDto.json @@ -0,0 +1,15 @@ +{ + "type": "VOICE_RELAY", + "voiceRelay": { + "endpoint": "wss://relay.example.com/agent", + "enableInterruptions": false, + "ttsVoice": "Emma", + "sttLanguage": "en-US", + "callHeaders": [ + { + "key": "customer-id", + "value": "12345" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AmdCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AmdCommandDto.json new file mode 100644 index 000000000..8e7e16b0e --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AmdCommandDto.json @@ -0,0 +1,53 @@ +{ + "command": "amd", + "events": { + "onHuman": [ + { + "command": "messages", + "messagesName": "my-messages", + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + }, + { + "type": "PLAY", + "play": { + "url": "https://example.com/audio/welcome.mp3" + } + } + ], + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + ], + "onMachine": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onBeep": [ + { + "command": "pause", + "durationMilliseconds": 1500 + } + ], + "onUnknown": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AmdEventsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AmdEventsDto.json new file mode 100644 index 000000000..3e2fbf090 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AmdEventsDto.json @@ -0,0 +1,50 @@ +{ + "onHuman": [ + { + "command": "messages", + "messagesName": "my-messages", + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + }, + { + "type": "PLAY", + "play": { + "url": "https://example.com/audio/welcome.mp3" + } + } + ], + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + ], + "onMachine": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onBeep": [ + { + "command": "pause", + "durationMilliseconds": 1500 + } + ], + "onUnknown": [ + { + "command": "hangup", + "callName": "origin" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AnswerCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AnswerCommandDto.json new file mode 100644 index 000000000..0cce78131 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/AnswerCommandDto.json @@ -0,0 +1,3 @@ +{ + "command": "answer" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/BridgeCallCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/BridgeCallCommandDto.json new file mode 100644 index 000000000..98342420b --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/BridgeCallCommandDto.json @@ -0,0 +1,4 @@ +{ + "command": "bridgeCall", + "bridgeName": "bridge" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/CallEventsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/CallEventsDto.json new file mode 100644 index 000000000..caec50db7 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/CallEventsDto.json @@ -0,0 +1,193 @@ +{ + "onAnswer": [ + { + "command": "answer" + }, + { + "command": "amd", + "events": { + "onHuman": [ + { + "command": "messages", + "messagesName": "my-messages", + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + }, + { + "type": "PLAY", + "play": { + "url": "https://example.com/audio/welcome.mp3" + } + } + ], + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + ], + "onMachine": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onBeep": [ + { + "command": "pause", + "durationMilliseconds": 1500 + } + ], + "onUnknown": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + }, + { + "command": "startRecording", + "recordingName": "my-recording", + "recordingOptions": { + "format": "MP3", + "recordingType": "COMBINED", + "destination": "AWS", + "destinationUrl": "s3://my-bucket/recordings", + "credentials": "access-key:secret-key", + "transcriptionOptions": { + "isEnabled": true, + "locale": "en-US" + } + }, + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onFailure": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + }, + { + "command": "menu", + "startMenu": "main", + "menus": { + "main": { + "prompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "repeatPrompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "inputTimeoutDurationSeconds": 10, + "repeatCount": 3, + "minimumInputLength": 1, + "maximumInputLength": 4, + "terminatingSequence": "#", + "inputMethods": [ + "DTMF" + ], + "matches": { + "1": [ + { + "command": "gotoMenu", + "menuName": "main" + } + ] + }, + "onFail": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + }, + { + "command": "bridgeCall", + "bridgeName": "bridge" + }, + { + "command": "webhook", + "webhookName": "my.custom.event", + "url": "https://example.com/events", + "fallbackUrl": "https://fallback.example.com/events" + }, + { + "command": "stopMessages", + "messagesName": "my-messages" + } + ], + "onBusy": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onReject": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onTimeout": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onHangup": [ + { + "command": "stopRecording", + "recordingName": "my-recording" + }, + { + "command": "hangup", + "callName": "origin" + } + ], + "onFailure": [ + { + "command": "hangup", + "callName": "origin" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/CustomEventCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/CustomEventCommandDto.json new file mode 100644 index 000000000..3d206d2d5 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/CustomEventCommandDto.json @@ -0,0 +1,6 @@ +{ + "command": "webhook", + "webhookName": "my.custom.event", + "url": "https://example.com/events", + "fallbackUrl": "https://fallback.example.com/events" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandDto.json new file mode 100644 index 000000000..bb82c7c69 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandDto.json @@ -0,0 +1,211 @@ +{ + "command": "dial", + "callName": "origin", + "from": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "to": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "dialTimeoutDurationSeconds": 30, + "maxCallDurationSeconds": 3600, + "events": { + "onAnswer": [ + { + "command": "answer" + }, + { + "command": "amd", + "events": { + "onHuman": [ + { + "command": "messages", + "messagesName": "my-messages", + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + }, + { + "type": "PLAY", + "play": { + "url": "https://example.com/audio/welcome.mp3" + } + } + ], + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + ], + "onMachine": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onBeep": [ + { + "command": "pause", + "durationMilliseconds": 1500 + } + ], + "onUnknown": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + }, + { + "command": "startRecording", + "recordingName": "my-recording", + "recordingOptions": { + "format": "MP3", + "recordingType": "COMBINED", + "destination": "AWS", + "destinationUrl": "s3://my-bucket/recordings", + "credentials": "access-key:secret-key", + "transcriptionOptions": { + "isEnabled": true, + "locale": "en-US" + } + }, + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onFailure": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + }, + { + "command": "menu", + "startMenu": "main", + "menus": { + "main": { + "prompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "repeatPrompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "inputTimeoutDurationSeconds": 10, + "repeatCount": 3, + "minimumInputLength": 1, + "maximumInputLength": 4, + "terminatingSequence": "#", + "inputMethods": [ + "DTMF" + ], + "matches": { + "1": [ + { + "command": "gotoMenu", + "menuName": "main" + } + ] + }, + "onFail": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } + }, + { + "command": "bridgeCall", + "bridgeName": "bridge" + }, + { + "command": "webhook", + "webhookName": "my.custom.event", + "url": "https://example.com/events", + "fallbackUrl": "https://fallback.example.com/events" + }, + { + "command": "stopMessages", + "messagesName": "my-messages" + } + ], + "onBusy": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onReject": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onTimeout": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onHangup": [ + { + "command": "stopRecording", + "recordingName": "my-recording" + }, + { + "command": "hangup", + "callName": "origin" + } + ], + "onFailure": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandSipDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandSipDto.json new file mode 100644 index 000000000..2f316bb2e --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandSipDto.json @@ -0,0 +1,24 @@ +{ + "command": "dial", + "callName": "origin", + "from": { + "type": "SIP", + "sip": { + "endpoint": "sip:alice@sip.example.com", + "displayName": "Alice" + } + }, + "to": { + "type": "SIP", + "sip": { + "endpoint": "sips:bob@sip.example.com", + "transport": "TLS", + "callHeaders": [ + { + "key": "X-Custom-Header", + "value": "custom-value" + } + ] + } + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandStreamDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandStreamDto.json new file mode 100644 index 000000000..db9cfc202 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandStreamDto.json @@ -0,0 +1,20 @@ +{ + "command": "dial", + "to": { + "type": "STREAM", + "stream": { + "endpoint": "wss://myapp.example.com/audio", + "streamOptions": { + "version": 1, + "codec": "PCM", + "sampleRate": 16000 + }, + "callHeaders": [ + { + "key": "customer-id", + "value": "12345" + } + ] + } + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandVoiceRelayDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandVoiceRelayDto.json new file mode 100644 index 000000000..c54ef6fd1 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/DialCommandVoiceRelayDto.json @@ -0,0 +1,18 @@ +{ + "command": "dial", + "to": { + "type": "VOICE_RELAY", + "voiceRelay": { + "endpoint": "wss://relay.example.com/agent", + "enableInterruptions": false, + "ttsVoice": "Emma", + "sttLanguage": "en-US", + "callHeaders": [ + { + "key": "customer-id", + "value": "12345" + } + ] + } + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/GotoMenuCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/GotoMenuCommandDto.json new file mode 100644 index 000000000..6c7fc5a84 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/GotoMenuCommandDto.json @@ -0,0 +1,4 @@ +{ + "command": "gotoMenu", + "menuName": "main" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/HangupCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/HangupCommandDto.json new file mode 100644 index 000000000..39c7fc5d1 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/HangupCommandDto.json @@ -0,0 +1,4 @@ +{ + "command": "hangup", + "callName": "origin" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuCommandDto.json new file mode 100644 index 000000000..4149f6fda --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuCommandDto.json @@ -0,0 +1,56 @@ +{ + "command": "menu", + "startMenu": "main", + "menus": { + "main": { + "prompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "repeatPrompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "inputTimeoutDurationSeconds": 10, + "repeatCount": 3, + "minimumInputLength": 1, + "maximumInputLength": 4, + "terminatingSequence": "#", + "inputMethods": [ + "DTMF" + ], + "matches": { + "1": [ + { + "command": "gotoMenu", + "menuName": "main" + } + ] + }, + "onFail": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuItemDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuItemDto.json new file mode 100644 index 000000000..6c8615bd8 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuItemDto.json @@ -0,0 +1,50 @@ +{ + "prompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "repeatPrompt": { + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] + }, + "inputTimeoutDurationSeconds": 10, + "repeatCount": 3, + "minimumInputLength": 1, + "maximumInputLength": 4, + "terminatingSequence": "#", + "inputMethods": [ + "DTMF" + ], + "matches": { + "1": [ + { + "command": "gotoMenu", + "menuName": "main" + } + ] + }, + "onFail": [ + { + "command": "hangup", + "callName": "origin" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuPromptDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuPromptDto.json new file mode 100644 index 000000000..ec40fd303 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MenuPromptDto.json @@ -0,0 +1,13 @@ +{ + "allowBargeIn": false, + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MessageEventsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MessageEventsDto.json new file mode 100644 index 000000000..7438c67be --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MessageEventsDto.json @@ -0,0 +1,8 @@ +{ + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MessagesCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MessagesCommandDto.json new file mode 100644 index 000000000..db7f63ea5 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/MessagesCommandDto.json @@ -0,0 +1,28 @@ +{ + "command": "messages", + "messagesName": "my-messages", + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + }, + { + "type": "PLAY", + "play": { + "url": "https://example.com/audio/welcome.mp3" + } + } + ], + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PauseCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PauseCommandDto.json new file mode 100644 index 000000000..f3132842a --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PauseCommandDto.json @@ -0,0 +1,4 @@ +{ + "command": "pause", + "durationMilliseconds": 1500 +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PlayDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PlayDto.json new file mode 100644 index 000000000..f0ce579c4 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PlayDto.json @@ -0,0 +1,3 @@ +{ + "url": "https://example.com/audio/welcome.mp3" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PlayMessageDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PlayMessageDto.json new file mode 100644 index 000000000..d60f1767c --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/PlayMessageDto.json @@ -0,0 +1,6 @@ +{ + "type": "PLAY", + "play": { + "url": "https://example.com/audio/welcome.mp3" + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/RecordingEventsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/RecordingEventsDto.json new file mode 100644 index 000000000..86727a803 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/RecordingEventsDto.json @@ -0,0 +1,14 @@ +{ + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onFailure": [ + { + "command": "hangup", + "callName": "origin" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/RecordingOptionsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/RecordingOptionsDto.json new file mode 100644 index 000000000..81111531b --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/RecordingOptionsDto.json @@ -0,0 +1,11 @@ +{ + "format": "MP3", + "recordingType": "COMBINED", + "destination": "AWS", + "destinationUrl": "s3://my-bucket/recordings", + "credentials": "access-key:secret-key", + "transcriptionOptions": { + "isEnabled": true, + "locale": "en-US" + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SayDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SayDto.json new file mode 100644 index 000000000..d781fc302 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SayDto.json @@ -0,0 +1,5 @@ +{ + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SayMessageDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SayMessageDto.json new file mode 100644 index 000000000..5ac574e4b --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SayMessageDto.json @@ -0,0 +1,8 @@ +{ + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StartRecordingCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StartRecordingCommandDto.json new file mode 100644 index 000000000..6f4c23146 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StartRecordingCommandDto.json @@ -0,0 +1,29 @@ +{ + "command": "startRecording", + "recordingName": "my-recording", + "recordingOptions": { + "format": "MP3", + "recordingType": "COMBINED", + "destination": "AWS", + "destinationUrl": "s3://my-bucket/recordings", + "credentials": "access-key:secret-key", + "transcriptionOptions": { + "isEnabled": true, + "locale": "en-US" + } + }, + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "onFailure": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StopMessagesCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StopMessagesCommandDto.json new file mode 100644 index 000000000..f70f3c940 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StopMessagesCommandDto.json @@ -0,0 +1,4 @@ +{ + "command": "stopMessages", + "messagesName": "my-messages" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StopRecordingCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StopRecordingCommandDto.json new file mode 100644 index 000000000..e0dd46e1d --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/StopRecordingCommandDto.json @@ -0,0 +1,4 @@ +{ + "command": "stopRecording", + "recordingName": "my-recording" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/TranscriptionOptionsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/TranscriptionOptionsDto.json new file mode 100644 index 000000000..0e6225682 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/TranscriptionOptionsDto.json @@ -0,0 +1,4 @@ +{ + "isEnabled": true, + "locale": "en-US" +} diff --git a/pom.xml b/pom.xml index 29cd923ff..b12927c63 100644 --- a/pom.xml +++ b/pom.xml @@ -295,6 +295,7 @@ com.sinch.sdk.e2e.domains.conversation.ConversationIT com.sinch.sdk.e2e.domains.sms.v1.SmsIT com.sinch.sdk.e2e.domains.voice.v1.VoiceIT + com.sinch.sdk.e2e.domains.voice.v2.VoiceV2IT com.sinch.sdk.e2e.domains.verification.v1.VerificationIT com.sinch.sdk.e2e.domains.numberlookup.v2.NumberLookupIT - 2.21.5 + 2.21.6 5.6.3 From b85f5cee4cd09473f71d49a23162d7c8747f8a57 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 1 Oct 2026 11:48:27 +0200 Subject: [PATCH 16/50] DEVEXP-1618: Remove idempotency key generation, name based region lookup and duplicated toString tests --- .../domains/voice/models/v2/VoiceRegion.java | 26 +------------ .../voice/models/v2/VoiceRegionTest.java | 39 ------------------- .../e2e/domains/voice/v2/BatchesSteps.java | 2 +- .../sdk/e2e/domains/voice/v2/CallsSteps.java | 6 ++- .../sinch/sdk/core/http/IdempotencyKey.java | 33 ---------------- .../sdk/core/http/IdempotencyKeyTest.java | 19 --------- .../api/v2/adapters/BatchesServiceImpl.java | 11 +----- .../api/v2/adapters/CallsServiceImpl.java | 11 +----- .../v2/batches/request/StartBatchRequest.java | 4 +- .../v2/calls/request/StartCallRequest.java | 4 +- .../request/StartBatchRequestDtoTest.java | 9 ----- .../request/StartCallRequestDtoTest.java | 9 ----- 12 files changed, 13 insertions(+), 160 deletions(-) delete mode 100644 client/src/test/java/com/sinch/sdk/domains/voice/models/v2/VoiceRegionTest.java delete mode 100644 core/src/main/com/sinch/sdk/core/http/IdempotencyKey.java delete mode 100644 core/src/test/java/com/sinch/sdk/core/http/IdempotencyKeyTest.java diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/VoiceRegion.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/VoiceRegion.java index 5f93f9b83..a957f56d6 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/models/v2/VoiceRegion.java +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/VoiceRegion.java @@ -3,9 +3,6 @@ import com.sinch.sdk.core.utils.EnumDynamic; import com.sinch.sdk.core.utils.EnumSupportDynamic; import java.util.Arrays; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; import java.util.stream.Stream; /** @@ -42,18 +39,6 @@ public class VoiceRegion extends EnumDynamic { VoiceRegion::new, Arrays.asList(GLOBAL, NORTH_AMERICA, SOUTH_AMERICA, EUROPE, ASIA_PACIFIC, AUSTRALIA)); - // Lets end users store the region by its constant name (e.g. "AUSTRALIA") instead of its value - private static final Map BY_NAME = new HashMap<>(); - - static { - BY_NAME.put("GLOBAL", GLOBAL); - BY_NAME.put("NORTH_AMERICA", NORTH_AMERICA); - BY_NAME.put("SOUTH_AMERICA", SOUTH_AMERICA); - BY_NAME.put("EUROPE", EUROPE); - BY_NAME.put("ASIA_PACIFIC", ASIA_PACIFIC); - BY_NAME.put("AUSTRALIA", AUSTRALIA); - } - private VoiceRegion(String value) { super(value); } @@ -70,18 +55,11 @@ public static Stream values() { /** * Get value from a string * - *

The string can be either the constant name (e.g. AUSTRALIA, case insensitive) - * or the region value (e.g. au1). - * - * @param value Constant name or region value + * @param value String identifier * @return Dynamic enum from value. A new enum is created if value is not yet registered */ public static VoiceRegion from(String value) { - if (null == value) { - return null; - } - VoiceRegion byName = BY_NAME.get(value.trim().toUpperCase(Locale.ROOT)); - return null != byName ? byName : ENUM_SUPPORT.from(value); + return ENUM_SUPPORT.from(value); } /** diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/VoiceRegionTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/VoiceRegionTest.java deleted file mode 100644 index f4d251395..000000000 --- a/client/src/test/java/com/sinch/sdk/domains/voice/models/v2/VoiceRegionTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.sinch.sdk.domains.voice.models.v2; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import org.junit.jupiter.api.Test; - -class VoiceRegionTest { - - @Test - void fromValue() { - assertEquals(VoiceRegion.AUSTRALIA, VoiceRegion.from("au1")); - } - - @Test - void fromName() { - assertEquals(VoiceRegion.GLOBAL, VoiceRegion.from("GLOBAL")); - assertEquals(VoiceRegion.NORTH_AMERICA, VoiceRegion.from("NORTH_AMERICA")); - assertEquals(VoiceRegion.SOUTH_AMERICA, VoiceRegion.from("SOUTH_AMERICA")); - assertEquals(VoiceRegion.EUROPE, VoiceRegion.from("EUROPE")); - assertEquals(VoiceRegion.ASIA_PACIFIC, VoiceRegion.from("ASIA_PACIFIC")); - assertEquals(VoiceRegion.AUSTRALIA, VoiceRegion.from("AUSTRALIA")); - } - - @Test - void fromNameIsCaseInsensitive() { - assertEquals(VoiceRegion.AUSTRALIA, VoiceRegion.from("australia")); - } - - @Test - void fromUnknownValue() { - assertEquals("foo", VoiceRegion.from("foo").value()); - } - - @Test - void fromNull() { - assertNull(VoiceRegion.from(null)); - } -} diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java index 8ad62f290..b619b28e3 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java @@ -61,7 +61,6 @@ public void start() { .build()) .build(); - // No Idempotency-Key set: the mock requires one, so this also checks the SDK generates it. startResponse = service.start( StartBatchRequest.builder() @@ -71,6 +70,7 @@ public void start() { Collections.singletonMap("toNumber", "+12017777777"), Collections.singletonMap("toNumber", "+12018888888"))) .setBatchOptions(BatchOptions.builder().setMaxCps(5).setTtlSeconds(600).build()) + .setIdempotencyKey("7b2e9d41-5c6a-4f80-b3d2-9e1a4c7f6b05") .build()); } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java index 749f9dc3e..6ef4ab484 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java @@ -64,10 +64,12 @@ public void start() { CallEvents.builder().setOnAnswer(Collections.singletonList(notification)).build()) .build(); - // No Idempotency-Key set: the mock requires one, so this also checks the SDK generates it. startResponse = service.start( - StartCallRequest.builder().setCommands(Collections.singletonList(dial)).build()); + StartCallRequest.builder() + .setCommands(Collections.singletonList(dial)) + .setIdempotencyKey("3f1c7a52-8d3e-4b9a-9c0e-2f6d1b7a4e21") + .build()); } @Then("^the response contains the information about the call started$") diff --git a/core/src/main/com/sinch/sdk/core/http/IdempotencyKey.java b/core/src/main/com/sinch/sdk/core/http/IdempotencyKey.java deleted file mode 100644 index aba657399..000000000 --- a/core/src/main/com/sinch/sdk/core/http/IdempotencyKey.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.sinch.sdk.core.http; - -import java.util.UUID; -import java.util.logging.Logger; - -/** - * Keys for the operations that accept an Idempotency-Key header. - * - *

A key identifies one operation: it is sent again on the retries of that operation and never - * reused for another one. - * - * @since 2.3 - */ -public final class IdempotencyKey { - - private static final Logger LOGGER = Logger.getLogger(IdempotencyKey.class.getName()); - - /** Name of the HTTP header carrying the key */ - public static final String HEADER = "Idempotency-Key"; - - private IdempotencyKey() {} - - /** - * Generate a new key: a random UUID, as recommended by the Sinch API standards. - * - * @return A new key, to use for one operation and its retries - */ - public static String generate() { - String key = UUID.randomUUID().toString(); - LOGGER.fine("Generated idempotency key: " + key); - return key; - } -} diff --git a/core/src/test/java/com/sinch/sdk/core/http/IdempotencyKeyTest.java b/core/src/test/java/com/sinch/sdk/core/http/IdempotencyKeyTest.java deleted file mode 100644 index 20f3782dd..000000000 --- a/core/src/test/java/com/sinch/sdk/core/http/IdempotencyKeyTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sinch.sdk.core.http; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -class IdempotencyKeyTest { - - // Sinch API standards: keys are 16 to 128 characters long - @Test - void generatedKeyLengthFollowsStandard() { - Assertions.assertThat(IdempotencyKey.generate()).hasSizeBetween(16, 128); - } - - // One key per operation: two calls must not return the same key - @Test - void generatedKeysAreDistinct() { - Assertions.assertThat(IdempotencyKey.generate()).isNotEqualTo(IdempotencyKey.generate()); - } -} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java index da12c9ed1..f8cf520e0 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java @@ -10,12 +10,10 @@ import com.sinch.sdk.core.http.HttpRequest; import com.sinch.sdk.core.http.HttpResponse; import com.sinch.sdk.core.http.HttpStatus; -import com.sinch.sdk.core.http.IdempotencyKey; import com.sinch.sdk.core.http.URLParameter; import com.sinch.sdk.core.http.URLParameterUtils; import com.sinch.sdk.core.http.URLPathUtils; import com.sinch.sdk.core.models.ServerConfiguration; -import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; @@ -118,14 +116,9 @@ private HttpRequest startRequestBuilder( true); } - // Without a key from the caller, one is generated here, once per call. The HTTP client - // re-sends this same request on every retry, so the key does not change between attempts. - String idempotencyKey = startBatchRequest.getIdempotencyKey(); - if (StringUtil.isEmpty(idempotencyKey)) { - idempotencyKey = IdempotencyKey.generate(); - } Map localVarHeaderParams = new HashMap<>(); - localVarHeaderParams.put(IdempotencyKey.HEADER, idempotencyKey); + if (startBatchRequest.getIdempotencyKey() != null) + localVarHeaderParams.put("Idempotency-Key", startBatchRequest.getIdempotencyKey()); final Collection localVarAccepts = Arrays.asList("application/json", "application/problem+json"); diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java index 82ddfd604..61ee593fc 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java @@ -10,12 +10,10 @@ import com.sinch.sdk.core.http.HttpRequest; import com.sinch.sdk.core.http.HttpResponse; import com.sinch.sdk.core.http.HttpStatus; -import com.sinch.sdk.core.http.IdempotencyKey; import com.sinch.sdk.core.http.URLParameter; import com.sinch.sdk.core.http.URLParameterUtils; import com.sinch.sdk.core.http.URLPathUtils; import com.sinch.sdk.core.models.ServerConfiguration; -import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; @@ -118,14 +116,9 @@ private HttpRequest startRequestBuilder( true); } - // Without a key from the caller, one is generated here, once per call. The HTTP client - // re-sends this same request on every retry, so the key does not change between attempts. - String idempotencyKey = startCallRequest.getIdempotencyKey(); - if (StringUtil.isEmpty(idempotencyKey)) { - idempotencyKey = IdempotencyKey.generate(); - } Map localVarHeaderParams = new HashMap<>(); - localVarHeaderParams.put(IdempotencyKey.HEADER, idempotencyKey); + if (startCallRequest.getIdempotencyKey() != null) + localVarHeaderParams.put("Idempotency-Key", startCallRequest.getIdempotencyKey()); final Collection localVarAccepts = Arrays.asList("application/json", "application/problem+json"); diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java index 6e4afb305..585f28705 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java @@ -61,9 +61,7 @@ public interface StartBatchRequest { * minutes, the server returns the cached response from the original request. Using a random UUID * (v4) is strongly recommended. * - *

Sent as the Idempotency-Key header. If not set, the SDK generates a random UUID - * for each call. Whether set or generated, the same key is sent on every automatic retry of that - * call. + *

Sent as the Idempotency-Key header. * * @return idempotencyKey */ diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java index de6d7f27d..1a43a8c53 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java @@ -35,9 +35,7 @@ public interface StartCallRequest { * minutes, the server returns the cached response from the original request. Using a random UUID * (v4) is strongly recommended. * - *

Sent as the Idempotency-Key header. If not set, the SDK generates a random UUID - * for each call. Whether set or generated, the same key is sent on every automatic retry of that - * call. + *

Sent as the Idempotency-Key header. * * @return idempotencyKey */ diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequestDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequestDtoTest.java index c91254ec3..325a5cbb7 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequestDtoTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequestDtoTest.java @@ -10,7 +10,6 @@ import java.util.HashMap; import java.util.Map; import org.json.JSONException; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; @@ -43,12 +42,4 @@ void serialize() throws JsonProcessingException, JSONException { JSONAssert.assertEquals(jsonStartBatchRequest, serializedString, true); } - - @Test - void toStringMasksRecordingCredentials() { - String string = expectedStartBatchRequest.toString(); - - Assertions.assertFalse(string.contains("access-key:secret-key"), string); - Assertions.assertTrue(string.contains("credentials: ***"), string); - } } diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestDtoTest.java index 6eb7d58ee..dd56cfddf 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestDtoTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequestDtoTest.java @@ -7,7 +7,6 @@ import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommandDtoTest; import java.util.Arrays; import org.json.JSONException; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; @@ -29,12 +28,4 @@ void serialize() throws JsonProcessingException, JSONException { JSONAssert.assertEquals(jsonStartCallRequest, serializedString, true); } - - @Test - void toStringMasksRecordingCredentials() { - String string = expectedStartCallRequest.toString(); - - Assertions.assertFalse(string.contains("access-key:secret-key"), string); - Assertions.assertTrue(string.contains("credentials: ***"), string); - } } From ee000434cfb069038fa928f505474d29e51c8621 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 1 Oct 2026 12:12:19 +0200 Subject: [PATCH 17/50] DEVEXP-1526: Address PR review comments --- CHANGELOG.md | 1 - README.md | 4 +-- .../sms/api/v1/adapters/SMSService.java | 5 ++- .../com/sinch/sdk/http/HttpClientApache.java | 35 ------------------- .../sinch/sdk/models/TransportSettings.java | 7 ++-- .../sdk/http/HttpClientApacheTimeoutTest.java | 6 ++-- 6 files changed, 11 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad0194b1..55d8ab4d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,6 @@ All notable changes to the **Sinch Java SDK** are documented in this file. ### SDK - **[feature]** `SinchClient` transport level settings: `TransportSettings` (`timeoutSeconds`, `useSinchAuth`) -- **[deprecation notice]** `HttpClientApache` constructors without a timeout, use `HttpClientApache(HttpProxyConfiguration, RetryManager, int)` - **[test]** Extend `ApplicationAuthManager` signed request validation coverage ### Verification diff --git a/README.md b/README.md index f50d733e6..bf84e01ee 100644 --- a/README.md +++ b/README.md @@ -550,7 +550,7 @@ SinchClient client = new SinchClient(configuration); | Field | Type | Default | Description | |-------|------|---------|-------------| | `timeoutSeconds` | `int` | `60` | HTTP I/O timeout in seconds. Pass `0` to disable the timeout. Negative values are rejected. | -| `useSinchAuth` | `boolean` | `true` | When `true`, OAuth-capable APIs fetch a Sinch access token. Set `false` to skip the token exchange: the SDK adds no `Authorization` header and the caller authorizes the requests. In that mode only `projectId` is required. | +| `useSinchAuth` | `boolean` | `true` | When `true`, OAuth-capable APIs fetch a Sinch access token. Set `false` to disable Sinch authentication: no token is fetched and no `Authorization` header is added. With unified credentials only `projectId` is then required. Has no effect with SMS service plan credentials. | ### Timeout @@ -574,7 +574,7 @@ SinchClient client = new SinchClient(configuration); ```java Configuration configuration = Configuration.builder() - .setProjectId(PARAM_PROJECT_ID) + ... .setTransportSettings( TransportSettings.builder() .setUseSinchAuth(false) diff --git a/client/src/main/com/sinch/sdk/domains/sms/api/v1/adapters/SMSService.java b/client/src/main/com/sinch/sdk/domains/sms/api/v1/adapters/SMSService.java index f38f49e8a..24f1cbdac 100644 --- a/client/src/main/com/sinch/sdk/domains/sms/api/v1/adapters/SMSService.java +++ b/client/src/main/com/sinch/sdk/domains/sms/api/v1/adapters/SMSService.java @@ -15,7 +15,6 @@ import com.sinch.sdk.models.Credentials; import com.sinch.sdk.models.SmsContext; import com.sinch.sdk.models.SmsServicePlanCredentials; -import com.sinch.sdk.models.TransportSettings; import com.sinch.sdk.models.UnifiedCredentials; import java.util.AbstractMap; import java.util.Collections; @@ -66,8 +65,8 @@ public SMSService( this.context = context; this.httpClientSupplier = httpClientSupplier; this.oAuthServer = null; - // no-op with service plan credentials: they do not use Sinch OAuth - this.useSinchAuth = TransportSettings.DEFAULT_USE_SINCH_AUTH; + // Sinch OAuth does not apply to service plan credentials + this.useSinchAuth = false; } @Override diff --git a/client/src/main/com/sinch/sdk/http/HttpClientApache.java b/client/src/main/com/sinch/sdk/http/HttpClientApache.java index 6b5b275d9..2a6a23066 100644 --- a/client/src/main/com/sinch/sdk/http/HttpClientApache.java +++ b/client/src/main/com/sinch/sdk/http/HttpClientApache.java @@ -17,7 +17,6 @@ import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.core.utils.Pair; import com.sinch.sdk.models.HttpProxyConfiguration; -import com.sinch.sdk.models.TransportSettings; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; @@ -74,40 +73,6 @@ public class HttpClientApache implements com.sinch.sdk.core.http.HttpClient, Ret private final RetryManager retryManager; - /** - * @deprecated Use {@link #HttpClientApache(HttpProxyConfiguration, RetryManager, int)}, which - * takes the transport settings explicitly. This one builds a direct connection with no retry - * and {@link TransportSettings#DEFAULT_TIMEOUT_SECONDS}. - */ - @Deprecated - public HttpClientApache() { - this(null, null, TransportSettings.DEFAULT_TIMEOUT_SECONDS); - } - - /** - * @param proxyConfiguration Proxy to route through, or {@code null} for a direct connection - * @deprecated Use {@link #HttpClientApache(HttpProxyConfiguration, RetryManager, int)}, which - * takes the transport settings explicitly. This one applies no retry and {@link - * TransportSettings#DEFAULT_TIMEOUT_SECONDS}. - */ - @Deprecated - public HttpClientApache(HttpProxyConfiguration proxyConfiguration) { - this(proxyConfiguration, null, TransportSettings.DEFAULT_TIMEOUT_SECONDS); - } - - /** - * @param proxyConfiguration Proxy to route through, or {@code null} for a direct connection - * @param retryManager Manager applied to rate-limited responses, or {@code null} to retry nothing - * @since 2.2 - * @deprecated Use {@link #HttpClientApache(HttpProxyConfiguration, RetryManager, int)}, which - * takes the transport settings explicitly. This one applies {@link - * TransportSettings#DEFAULT_TIMEOUT_SECONDS}. - */ - @Deprecated - public HttpClientApache(HttpProxyConfiguration proxyConfiguration, RetryManager retryManager) { - this(proxyConfiguration, retryManager, TransportSettings.DEFAULT_TIMEOUT_SECONDS); - } - /** * @param proxyConfiguration Proxy to route through, or {@code null} for a direct connection * @param retryManager Manager applied to rate-limited responses, or {@code null} to retry nothing diff --git a/client/src/main/com/sinch/sdk/models/TransportSettings.java b/client/src/main/com/sinch/sdk/models/TransportSettings.java index 971cc9f62..8a21c0439 100644 --- a/client/src/main/com/sinch/sdk/models/TransportSettings.java +++ b/client/src/main/com/sinch/sdk/models/TransportSettings.java @@ -46,8 +46,7 @@ public int getTimeoutSeconds() { /** * Whether the endpoints supporting it authenticate against the Sinch auth server. * - * @return {@code true} when the SDK fetches a Sinch access token and adds the {@code - * Authorization} header, {@code false} when the caller authorizes the requests + * @return {@code true} when Sinch authentication is enabled, {@code false} when it is disabled * @since 2.3 */ public boolean getUseSinchAuth() { @@ -121,8 +120,8 @@ public Builder setTimeoutSeconds(Integer timeoutSeconds) { /** * Set whether the endpoints supporting it authenticate against the Sinch auth server * - * @param useSinchAuth {@code true} to let the SDK fetch a Sinch access token and add the {@code - * Authorization} header, {@code false} to authorize the requests yourself + * @param useSinchAuth {@code true} (default) to authenticate with a Sinch access token, {@code + * false} to disable Sinch authentication * @return Current builder * @see TransportSettings#getUseSinchAuth() getter * @since 2.3 diff --git a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTimeoutTest.java b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTimeoutTest.java index 61ffd1a62..a14a07a56 100644 --- a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTimeoutTest.java +++ b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTimeoutTest.java @@ -7,6 +7,7 @@ import com.sinch.sdk.core.http.HttpRequest; import com.sinch.sdk.core.http.HttpResponse; import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.models.TransportSettings; import java.io.IOException; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; @@ -61,11 +62,12 @@ void aResponseInsideTheTimeoutSucceeds() throws Exception { } @Test - @SuppressWarnings("deprecation") void theDefaultTimeoutIsUsedWhenNoneIsGiven() throws Exception { server.enqueue(new MockResponse().setBody("{}")); - try (HttpClientApache client = new HttpClientApache(null, RetryManager.NO_RETRY)) { + try (HttpClientApache client = + new HttpClientApache( + null, RetryManager.NO_RETRY, TransportSettings.DEFAULTS.getTimeoutSeconds())) { assertEquals(200, invoke(client).getCode()); } } From 52a56508a7def68b403efd3a94ce1862bc768fa3 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 1 Oct 2026 15:50:04 +0200 Subject: [PATCH 18/50] DEVEXP-1526: Remove duplicated default timeout test --- .../sinch/sdk/http/HttpClientApacheTimeoutTest.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTimeoutTest.java b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTimeoutTest.java index a14a07a56..7ab1d60ba 100644 --- a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTimeoutTest.java +++ b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheTimeoutTest.java @@ -7,7 +7,6 @@ import com.sinch.sdk.core.http.HttpRequest; import com.sinch.sdk.core.http.HttpResponse; import com.sinch.sdk.core.models.ServerConfiguration; -import com.sinch.sdk.models.TransportSettings; import java.io.IOException; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; @@ -60,15 +59,4 @@ void aResponseInsideTheTimeoutSucceeds() throws Exception { assertEquals(200, invoke(client).getCode()); } } - - @Test - void theDefaultTimeoutIsUsedWhenNoneIsGiven() throws Exception { - server.enqueue(new MockResponse().setBody("{}")); - - try (HttpClientApache client = - new HttpClientApache( - null, RetryManager.NO_RETRY, TransportSettings.DEFAULTS.getTimeoutSeconds())) { - assertEquals(200, invoke(client).getCode()); - } - } } From 04d47cd0d687c460ee1cc09f541b3afb0a1d224a Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Mon, 5 Oct 2026 16:57:27 +0200 Subject: [PATCH 19/50] DEVEXP-1618: Map Voice V2 error responses and snippet modification --- .../core/exceptions/ApiExceptionBuilder.java | 28 +++++++++++++++++++ .../exceptions/ApiExceptionBuilderTest.java | 15 ++++++++++ .../src/main/java/voice/v2/batches/Start.java | 12 ++++---- .../src/main/java/voice/v2/calls/Start.java | 12 ++++---- .../domains/voice/v2/error-response.json | 6 ++++ 5 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/error-response.json diff --git a/core/src/main/com/sinch/sdk/core/exceptions/ApiExceptionBuilder.java b/core/src/main/com/sinch/sdk/core/exceptions/ApiExceptionBuilder.java index 4ffdbbef5..e4195d130 100644 --- a/core/src/main/com/sinch/sdk/core/exceptions/ApiExceptionBuilder.java +++ b/core/src/main/com/sinch/sdk/core/exceptions/ApiExceptionBuilder.java @@ -34,6 +34,11 @@ public static ApiException build(String message, int code, Map mapped } exception = getExceptionFromVerificationError(mappedResponse); + if (exception.isPresent()) { + return exception.get(); + } + + exception = getExceptionFromVoiceV2Error(code, mappedResponse); return exception.orElseGet(() -> new ApiException(code, message)); } @@ -129,6 +134,29 @@ private static Optional getExceptionFromVerificationError( codeValue, String.format("%s (reference=%s)", messageValue, referenceValue))); } + private static Optional getExceptionFromVoiceV2Error( + int intCode, Map mappedResponse) { + + // excepted Voice V2 API errors have following form (RFC 9457 Problem Details) + // "type": string, + // "title": string, + // "detail": string, + // "instance": string + + if (null == mappedResponse) { + return Optional.empty(); + } + + Object title = mappedResponse.get("title"); + Object detail = mappedResponse.get("detail"); + + if (null == title || null == detail) { + return Optional.empty(); + } + + return Optional.of(new ApiException(intCode, String.format("%s: %s", title, detail))); + } + private static String extractErrorDetails(Map errorContent) { // API is having a "details" field Collection details = (Collection) errorContent.get("details"); diff --git a/core/src/test/java/com/sinch/sdk/core/exceptions/ApiExceptionBuilderTest.java b/core/src/test/java/com/sinch/sdk/core/exceptions/ApiExceptionBuilderTest.java index 37e8d55c5..f446f32a2 100644 --- a/core/src/test/java/com/sinch/sdk/core/exceptions/ApiExceptionBuilderTest.java +++ b/core/src/test/java/com/sinch/sdk/core/exceptions/ApiExceptionBuilderTest.java @@ -30,6 +30,9 @@ class ApiExceptionBuilderTest extends BaseTest { @GivenTextResource("/domains/verification/v1/error-response.json") String verificationError; + @GivenTextResource("/domains/voice/v2/error-response.json") + String voiceV2Error; + @Test void buildFromObject() throws JsonProcessingException { ApiException e = @@ -81,4 +84,16 @@ void verificationError() throws JsonProcessingException { assertEquals("Requested resource was not found. (reference=a reference)", e.getMessage()); assertEquals(40400, e.getCode()); } + + @Test + void voiceV2Error() throws JsonProcessingException { + ApiException e = + ApiExceptionBuilder.build( + "message", + 400, + Mapper.getInstance() + .readValue(voiceV2Error, new TypeReference>() {})); + assertEquals("Bad Request: The request body is invalid or malformed.", e.getMessage()); + assertEquals(400, e.getCode()); + } } diff --git a/examples/snippets/src/main/java/voice/v2/batches/Start.java b/examples/snippets/src/main/java/voice/v2/batches/Start.java index 70d1855fc..9c43c408b 100644 --- a/examples/snippets/src/main/java/voice/v2/batches/Start.java +++ b/examples/snippets/src/main/java/voice/v2/batches/Start.java @@ -15,6 +15,7 @@ import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents; import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand; import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage; import com.sinch.sdk.models.Configuration; @@ -55,7 +56,7 @@ public static void main(String[] args) { BatchesService batchesService = client.voice().v2().batches(); - // Played to each recipient once their call is answered + // Played to each recipient once their call is answered, then the call is hung up MessagesCommand greeting = MessagesCommand.builder() .setMessages( @@ -64,6 +65,10 @@ public static void main(String[] args) { .setText("Hello, your call is now connected.") .setVoiceName("Emma") .build())) + .setEvents( + MessageEvents.builder() + .setOnFinish(Collections.singletonList(HangupCommand.HANGUP_COMMAND)) + .build()) .build(); // "@toNumber" is a placeholder, replaced in each call by that call's parameter value @@ -73,10 +78,7 @@ public static void main(String[] args) { .setFrom(Phone.builder().setNumber(sinchPhoneNumber).build()) .setTo(Phone.builder().setNumber("@toNumber").build()) .setEvents( - CallEvents.builder() - .setOnAnswer(Collections.singletonList(greeting)) - .setOnHangup(Collections.singletonList(HangupCommand.builder().build())) - .build()) + CallEvents.builder().setOnAnswer(Collections.singletonList(greeting)).build()) .build(); StartBatchRequest request = diff --git a/examples/snippets/src/main/java/voice/v2/calls/Start.java b/examples/snippets/src/main/java/voice/v2/calls/Start.java index 796c9ac6e..775928a00 100644 --- a/examples/snippets/src/main/java/voice/v2/calls/Start.java +++ b/examples/snippets/src/main/java/voice/v2/calls/Start.java @@ -15,6 +15,7 @@ import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents; import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand; import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage; import com.sinch.sdk.models.Configuration; @@ -48,7 +49,7 @@ public static void main(String[] args) { CallsService callsService = client.voice().v2().calls(); - // Played to the recipient once the call is answered + // Played to the recipient once the call is answered, then the call is hung up MessagesCommand greeting = MessagesCommand.builder() .setMessages( @@ -57,6 +58,10 @@ public static void main(String[] args) { .setText("Hello, your call is now connected.") .setVoiceName("Emma") .build())) + .setEvents( + MessageEvents.builder() + .setOnFinish(Collections.singletonList(HangupCommand.HANGUP_COMMAND)) + .build()) .build(); DialCommand dial = @@ -65,10 +70,7 @@ public static void main(String[] args) { .setFrom(Phone.builder().setNumber(sinchPhoneNumber).build()) .setTo(Phone.builder().setNumber(recipientPhoneNumber).build()) .setEvents( - CallEvents.builder() - .setOnAnswer(Collections.singletonList(greeting)) - .setOnHangup(Collections.singletonList(HangupCommand.builder().build())) - .build()) + CallEvents.builder().setOnAnswer(Collections.singletonList(greeting)).build()) .build(); StartCallRequest request = diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/error-response.json b/openapi-contracts/src/test/resources/domains/voice/v2/error-response.json new file mode 100644 index 000000000..169f187cd --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/error-response.json @@ -0,0 +1,6 @@ +{ + "type": "https://api.sinch.com/docs/errors/bad-request", + "title": "Bad Request", + "detail": "The request body is invalid or malformed.", + "instance": "/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls" +} From 1514280ea0618b731da174d6db2e073961e30199 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Tue, 6 Oct 2026 00:28:36 +0200 Subject: [PATCH 20/50] DEVEXP-1623: Add Voice v2 batches.get endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/BatchesServiceTest.java | 58 ++++ .../e2e/domains/voice/v2/BatchesSteps.java | 26 ++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 8 +- .../snippets/src/main/java/voice/README.md | 1 + .../src/main/java/voice/v2/batches/Get.java | 47 +++ .../domains/voice/api/v2/BatchesService.java | 14 + .../api/v2/adapters/BatchesServiceImpl.java | 65 ++++ .../v2/batches/response/BatchSummary.java | 203 +++++++++++ .../v2/batches/response/BatchSummaryImpl.java | 328 ++++++++++++++++++ .../batches/response/BatchSummaryDtoTest.java | 36 ++ .../v2/batches/response/BatchSummaryDto.json | 11 + 12 files changed, 798 insertions(+), 1 deletion(-) create mode 100644 examples/snippets/src/main/java/voice/v2/batches/Get.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummary.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummaryImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummaryDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSummaryDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b1463e3b..980d251f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,8 +21,10 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Support [Voice API v2](https://developers.sinch.com/docs/voice-2.0), exposed under `voice().v2()` - **[feature]** Voice V2 Calls API: `start` operation to create an outbound call - **[feature]** Voice V2 Batches API: `start` operation to create a batch of outbound calls +- **[feature]** Voice V2 Batches API: `get` operation to retrieve a batch summary - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/batches/Start` snippet +- **[doc]** New `voice/v2/batches/Get` snippet ## v2.2.0 - 2026-09-17 diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java index 45d69d790..839810308 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java @@ -23,6 +23,8 @@ import com.sinch.sdk.domains.voice.api.v2.BatchesService; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummaryDtoTest; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponseDtoTest; import java.util.Arrays; @@ -54,6 +56,9 @@ public class BatchesServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/batches/response/StartBatchResponseDto.json") String jsonStartBatchResponseDto; + @GivenTextResource("/domains/voice/v2/batches/response/BatchSummaryDto.json") + String jsonBatchSummaryDto; + @BeforeEach public void initMocks() { service = @@ -120,4 +125,57 @@ void startMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + + @Test + void get() throws ApiException { + + String batchId = BatchSummaryDtoTest.expectedBatchSummary.getBatchId(); + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/batches/" + + URLPathUtils.encodePathSegment(batchId), + HttpMethod.GET, + Collections.emptyList(), + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonBatchSummaryDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + BatchSummary response = service.get(batchId); + + TestHelpers.recursiveEquals(response, BatchSummaryDtoTest.expectedBatchSummary); + } + + @Test + void getMissingBatchIdThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.get(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void getMissingProjectIdThrows() { + + BatchesService serviceWithoutProjectId = + new BatchesServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, () -> serviceWithoutProjectId.get("01BX5ZZKBKACTAV9WEVGEMMVRC")); + + Assertions.assertEquals(400, thrown.getCode()); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java index b619b28e3..9644e40f6 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java @@ -4,6 +4,7 @@ import com.sinch.sdk.domains.voice.api.v2.BatchesService; import com.sinch.sdk.domains.voice.models.v2.batches.request.BatchOptions; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; import com.sinch.sdk.domains.voice.models.v2.destination.Phone; import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents; @@ -16,6 +17,7 @@ import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Assertions; @@ -24,6 +26,7 @@ public class BatchesSteps { BatchesService service; StartBatchResponse startResponse; + BatchSummary getResponse; @Given("^the Voice-V2 service \"Batches\" is available$") public void serviceAvailable() { @@ -85,4 +88,27 @@ public void startResult() { TestHelpers.recursiveEquals(startResponse, expected); } + + @When("^I send a request to get a batch call summary$") + public void get() { + getResponse = service.get("01M144V4N3GSTNVJ3V32TD7H9A"); + } + + @Then("^the response contains the batch call summary$") + public void getResult() { + BatchSummary expected = + BatchSummary.builder() + .setBatchId("01M144V4N3GSTNVJ3V32TD7H9A") + .setSessionCount(2) + .setEndTime(Instant.parse("2026-08-28T12:17:49Z")) + .setQueued(0) + .setInProgress(0) + .setCompleted(2) + .setExpired(0) + .setTtlSeconds(1800) + .setRequestedCps(5) + .build(); + + TestHelpers.recursiveEquals(getResponse, expected); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 14d962f4e..6d718aa3d 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -13,6 +13,10 @@ /** * Only the scenarios of the operations this SDK implements. Widen the resource and the name filter * as the other Voice V2 operations land. + * + *

The filter matches scenario names across both features, and both use the same {@code [Get]} + * prefix. Batches' {@code [Get]} is therefore matched by its full name, so that the calls {@code + * [Get]} scenario (not implemented yet) stays out. */ @Suite @SuiteDisplayName("Voice V2") @@ -20,6 +24,8 @@ @SelectClasspathResource("features/voice-v2/calls.feature") @SelectClasspathResource("features/voice-v2/batches.feature") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.sinch.sdk.e2e.domains.voice.v2") -@ConfigurationParameter(key = FILTER_NAME_PROPERTY_NAME, value = "^\\[Start\\].*") +@ConfigurationParameter( + key = FILTER_NAME_PROPERTY_NAME, + value = "^\\[Start\\].*|^\\[Get\\] get a batch call summary$") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index fd76dcd8b..b3f509063 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -27,3 +27,4 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/calls/Start](./v2/calls/Start.java) - Batches - [voice/v2/batches/Start](./v2/batches/Start.java) + - [voice/v2/batches/Get](./v2/batches/Get.java) diff --git a/examples/snippets/src/main/java/voice/v2/batches/Get.java b/examples/snippets/src/main/java/voice/v2/batches/Get.java new file mode 100644 index 000000000..73f6450e2 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/batches/Get.java @@ -0,0 +1,47 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.batches; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.BatchesService; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class Get { + + private static final Logger LOGGER = Logger.getLogger(Get.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the batch to get a summary for + String batchId = "BATCH_ID"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + BatchesService batchesService = client.voice().v2().batches(); + + LOGGER.info(String.format("Get summary for batch with ID '%s'", batchId)); + + BatchSummary response = batchesService.get(batchId); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java index 43896ad0f..af37482f9 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java @@ -3,6 +3,7 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; /** Batches Service */ @@ -41,4 +42,17 @@ public interface BatchesService { StartBatchResponse start( StartBatchQueryParameters queryParameter, StartBatchRequest startBatchRequest) throws ApiException; + + /** + * Get a batch summary + * + *

Retrieve a summary of a batch call operation, including statistics on completed, + * in-progress, queued and expired call sessions. + * + * @param batchId The ID of the batch, as returned by {@link #start} (required) + * @return BatchSummary + * @throws ApiException if fails to make API call + * @since 2.3 + */ + BatchSummary get(String batchId) throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java index f8cf520e0..aeedd968e 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java @@ -16,6 +16,7 @@ import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; import java.util.ArrayList; import java.util.Arrays; @@ -138,4 +139,68 @@ private HttpRequest startRequestBuilder( localVarContentTypes, localVarAuthNames); } + + @Override + public BatchSummary get(String batchId) throws ApiException { + + LOGGER.finest("[get]" + " " + "batchId: " + batchId); + + HttpRequest httpRequest = getRequestBuilder(batchId); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest getRequestBuilder(String batchId) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling get"); + } + // verify the required parameter 'batchId' is set + if (batchId == null) { + throw new ApiException(400, "Missing the required parameter 'batchId' when calling get"); + } + + String localVarPath = + "/v2/projects/{projectId}/batches/{batchId}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "batchId" + "\\}", URLPathUtils.encodePathSegment(batchId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.GET, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummary.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummary.java new file mode 100644 index 000000000..022c5677d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummary.java @@ -0,0 +1,203 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.time.Instant; + +/** + * Summary of a batch call operation, including counts of call sessions by state and overall + * progress. + */ +@JsonDeserialize(builder = BatchSummaryImpl.Builder.class) +public interface BatchSummary { + + /** + * Unique identifier of the batch call operation (ULID). + * + *

Use this value to retrieve the batch details or stop processing the batch. + * + *

Field is required + * + * @return batchId + */ + String getBatchId(); + + /** + * Total number of call sessions requested in this batch. + * + *

Field is required + * + * @return sessionCount + */ + Integer getSessionCount(); + + /** + * Timestamp (RFC 3339) when the batch finished processing (all call sessions reached a final + * state). + * + *

Omitted if the batch is still in progress or has not completed yet. + * + * @return endTime + */ + Instant getEndTime(); + + /** + * Number of call sessions that are queued and waiting to be initiated (not yet in progress). + * + *

Field is required + * + * @return queued + */ + Integer getQueued(); + + /** + * Number of call sessions that are in progress. + * + *

Field is required + * + * @return inProgress + */ + Integer getInProgress(); + + /** + * Number of call sessions that have completed successfully. + * + *

Field is required + * + * @return completed + */ + Integer getCompleted(); + + /** + * Number of queued call sessions that were not initiated before the batch TTL ({@link + * #getTtlSeconds()}) elapsed and therefore expired. + * + *

Field is required + * + * @return expired + */ + Integer getExpired(); + + /** + * Batch time-to-live (TTL) in seconds. The maximum amount of time the platform will keep + * attempting to start queued call sessions in this batch. + * + *

When the TTL expires, any call sessions that have not yet been initiated will stop being + * processed (calls already in progress are not affected). + * + * @return ttlSeconds + */ + Integer getTtlSeconds(); + + /** + * Requested maximum call initiation rate, in calls per second (CPS), for this batch. + * + *

Actual CPS may be lower depending on routing, carrier, and platform capacity, as well as + * account limitations. + * + *

Field is required + * + * @return requestedCps + */ + Integer getRequestedCps(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new BatchSummaryImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param batchId see getter + * @return Current builder + * @see #getBatchId + */ + Builder setBatchId(String batchId); + + /** + * see getter + * + * @param sessionCount see getter + * @return Current builder + * @see #getSessionCount + */ + Builder setSessionCount(Integer sessionCount); + + /** + * see getter + * + * @param endTime see getter + * @return Current builder + * @see #getEndTime + */ + Builder setEndTime(Instant endTime); + + /** + * see getter + * + * @param queued see getter + * @return Current builder + * @see #getQueued + */ + Builder setQueued(Integer queued); + + /** + * see getter + * + * @param inProgress see getter + * @return Current builder + * @see #getInProgress + */ + Builder setInProgress(Integer inProgress); + + /** + * see getter + * + * @param completed see getter + * @return Current builder + * @see #getCompleted + */ + Builder setCompleted(Integer completed); + + /** + * see getter + * + * @param expired see getter + * @return Current builder + * @see #getExpired + */ + Builder setExpired(Integer expired); + + /** + * see getter + * + * @param ttlSeconds see getter + * @return Current builder + * @see #getTtlSeconds + */ + Builder setTtlSeconds(Integer ttlSeconds); + + /** + * see getter + * + * @param requestedCps see getter + * @return Current builder + * @see #getRequestedCps + */ + Builder setRequestedCps(Integer requestedCps); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + BatchSummary build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummaryImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummaryImpl.java new file mode 100644 index 000000000..74884e9be --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummaryImpl.java @@ -0,0 +1,328 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.time.Instant; +import java.util.Objects; + +@JsonPropertyOrder({ + BatchSummaryImpl.JSON_PROPERTY_BATCH_ID, + BatchSummaryImpl.JSON_PROPERTY_SESSION_COUNT, + BatchSummaryImpl.JSON_PROPERTY_END_TIME, + BatchSummaryImpl.JSON_PROPERTY_QUEUED, + BatchSummaryImpl.JSON_PROPERTY_IN_PROGRESS, + BatchSummaryImpl.JSON_PROPERTY_COMPLETED, + BatchSummaryImpl.JSON_PROPERTY_EXPIRED, + BatchSummaryImpl.JSON_PROPERTY_TTL_SECONDS, + BatchSummaryImpl.JSON_PROPERTY_REQUESTED_CPS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class BatchSummaryImpl implements BatchSummary { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_BATCH_ID = "batchId"; + + private OptionalValue batchId; + + public static final String JSON_PROPERTY_SESSION_COUNT = "sessionCount"; + + private OptionalValue sessionCount; + + public static final String JSON_PROPERTY_END_TIME = "endTime"; + + private OptionalValue endTime; + + public static final String JSON_PROPERTY_QUEUED = "queued"; + + private OptionalValue queued; + + public static final String JSON_PROPERTY_IN_PROGRESS = "inProgress"; + + private OptionalValue inProgress; + + public static final String JSON_PROPERTY_COMPLETED = "completed"; + + private OptionalValue completed; + + public static final String JSON_PROPERTY_EXPIRED = "expired"; + + private OptionalValue expired; + + public static final String JSON_PROPERTY_TTL_SECONDS = "ttlSeconds"; + + private OptionalValue ttlSeconds; + + public static final String JSON_PROPERTY_REQUESTED_CPS = "requestedCps"; + + private OptionalValue requestedCps; + + public BatchSummaryImpl() {} + + protected BatchSummaryImpl( + OptionalValue batchId, + OptionalValue sessionCount, + OptionalValue endTime, + OptionalValue queued, + OptionalValue inProgress, + OptionalValue completed, + OptionalValue expired, + OptionalValue ttlSeconds, + OptionalValue requestedCps) { + this.batchId = batchId; + this.sessionCount = sessionCount; + this.endTime = endTime; + this.queued = queued; + this.inProgress = inProgress; + this.completed = completed; + this.expired = expired; + this.ttlSeconds = ttlSeconds; + this.requestedCps = requestedCps; + } + + @JsonIgnore + public String getBatchId() { + return batchId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_BATCH_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue batchId() { + return batchId; + } + + @JsonIgnore + public Integer getSessionCount() { + return sessionCount.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SESSION_COUNT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue sessionCount() { + return sessionCount; + } + + @JsonIgnore + public Instant getEndTime() { + return endTime.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_END_TIME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue endTime() { + return endTime; + } + + @JsonIgnore + public Integer getQueued() { + return queued.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_QUEUED) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue queued() { + return queued; + } + + @JsonIgnore + public Integer getInProgress() { + return inProgress.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_IN_PROGRESS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue inProgress() { + return inProgress; + } + + @JsonIgnore + public Integer getCompleted() { + return completed.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMPLETED) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue completed() { + return completed; + } + + @JsonIgnore + public Integer getExpired() { + return expired.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_EXPIRED) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue expired() { + return expired; + } + + @JsonIgnore + public Integer getTtlSeconds() { + return ttlSeconds.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TTL_SECONDS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue ttlSeconds() { + return ttlSeconds; + } + + @JsonIgnore + public Integer getRequestedCps() { + return requestedCps.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_REQUESTED_CPS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue requestedCps() { + return requestedCps; + } + + /** Return true if this BatchSummary object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BatchSummaryImpl batchSummary = (BatchSummaryImpl) o; + return Objects.equals(this.batchId, batchSummary.batchId) + && Objects.equals(this.sessionCount, batchSummary.sessionCount) + && Objects.equals(this.endTime, batchSummary.endTime) + && Objects.equals(this.queued, batchSummary.queued) + && Objects.equals(this.inProgress, batchSummary.inProgress) + && Objects.equals(this.completed, batchSummary.completed) + && Objects.equals(this.expired, batchSummary.expired) + && Objects.equals(this.ttlSeconds, batchSummary.ttlSeconds) + && Objects.equals(this.requestedCps, batchSummary.requestedCps); + } + + @Override + public int hashCode() { + return Objects.hash( + batchId, + sessionCount, + endTime, + queued, + inProgress, + completed, + expired, + ttlSeconds, + requestedCps); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class BatchSummaryImpl {\n"); + sb.append(" batchId: ").append(toIndentedString(batchId)).append("\n"); + sb.append(" sessionCount: ").append(toIndentedString(sessionCount)).append("\n"); + sb.append(" endTime: ").append(toIndentedString(endTime)).append("\n"); + sb.append(" queued: ").append(toIndentedString(queued)).append("\n"); + sb.append(" inProgress: ").append(toIndentedString(inProgress)).append("\n"); + sb.append(" completed: ").append(toIndentedString(completed)).append("\n"); + sb.append(" expired: ").append(toIndentedString(expired)).append("\n"); + sb.append(" ttlSeconds: ").append(toIndentedString(ttlSeconds)).append("\n"); + sb.append(" requestedCps: ").append(toIndentedString(requestedCps)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements BatchSummary.Builder { + OptionalValue batchId = OptionalValue.empty(); + OptionalValue sessionCount = OptionalValue.empty(); + OptionalValue endTime = OptionalValue.empty(); + OptionalValue queued = OptionalValue.empty(); + OptionalValue inProgress = OptionalValue.empty(); + OptionalValue completed = OptionalValue.empty(); + OptionalValue expired = OptionalValue.empty(); + OptionalValue ttlSeconds = OptionalValue.empty(); + OptionalValue requestedCps = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_BATCH_ID, required = true) + public Builder setBatchId(String batchId) { + this.batchId = OptionalValue.of(batchId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SESSION_COUNT, required = true) + public Builder setSessionCount(Integer sessionCount) { + this.sessionCount = OptionalValue.of(sessionCount); + return this; + } + + @JsonProperty(JSON_PROPERTY_END_TIME) + public Builder setEndTime(Instant endTime) { + this.endTime = OptionalValue.of(endTime); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_QUEUED, required = true) + public Builder setQueued(Integer queued) { + this.queued = OptionalValue.of(queued); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_IN_PROGRESS, required = true) + public Builder setInProgress(Integer inProgress) { + this.inProgress = OptionalValue.of(inProgress); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_COMPLETED, required = true) + public Builder setCompleted(Integer completed) { + this.completed = OptionalValue.of(completed); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_EXPIRED, required = true) + public Builder setExpired(Integer expired) { + this.expired = OptionalValue.of(expired); + return this; + } + + @JsonProperty(JSON_PROPERTY_TTL_SECONDS) + public Builder setTtlSeconds(Integer ttlSeconds) { + this.ttlSeconds = OptionalValue.of(ttlSeconds); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_REQUESTED_CPS, required = true) + public Builder setRequestedCps(Integer requestedCps) { + this.requestedCps = OptionalValue.of(requestedCps); + return this; + } + + public BatchSummary build() { + return new BatchSummaryImpl( + batchId, + sessionCount, + endTime, + queued, + inProgress, + completed, + expired, + ttlSeconds, + requestedCps); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummaryDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummaryDtoTest.java new file mode 100644 index 000000000..5dee722e9 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummaryDtoTest.java @@ -0,0 +1,36 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class BatchSummaryDtoTest extends BaseTest { + + public static final BatchSummary expectedBatchSummary = + BatchSummary.builder() + .setBatchId("01BX5ZZKBKACTAV9WEVGEMMVRC") + .setSessionCount(3) + .setEndTime(Instant.parse("2026-08-28T12:17:49Z")) + .setQueued(1) + .setInProgress(1) + .setCompleted(1) + .setExpired(0) + .setTtlSeconds(3600) + .setRequestedCps(10) + .build(); + + @GivenTextResource("/domains/voice/v2/batches/response/BatchSummaryDto.json") + String jsonBatchSummary; + + @Test + void deserialize() throws JsonProcessingException { + BatchSummary deserialized = objectMapper.readValue(jsonBatchSummary, BatchSummary.class); + + TestHelpers.recursiveEquals(deserialized, expectedBatchSummary); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSummaryDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSummaryDto.json new file mode 100644 index 000000000..fa589a742 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSummaryDto.json @@ -0,0 +1,11 @@ +{ + "batchId": "01BX5ZZKBKACTAV9WEVGEMMVRC", + "sessionCount": 3, + "endTime": "2026-08-28T12:17:49Z", + "queued": 1, + "inProgress": 1, + "completed": 1, + "expired": 0, + "ttlSeconds": 3600, + "requestedCps": 10 +} From 5b0ad598205c3105764042a1292e144b86e8699d Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Tue, 6 Oct 2026 10:22:06 +0200 Subject: [PATCH 21/50] DEVEXP-1623: Add Voice v2 batches.stop endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/BatchesServiceTest.java | 58 ++++++++++++ .../e2e/domains/voice/v2/BatchesSteps.java | 15 ++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 3 +- .../snippets/src/main/java/voice/README.md | 1 + .../src/main/java/voice/v2/batches/Stop.java | 47 ++++++++++ .../domains/voice/api/v2/BatchesService.java | 14 +++ .../api/v2/adapters/BatchesServiceImpl.java | 65 ++++++++++++++ .../batches/response/BatchStopResponse.java | 77 ++++++++++++++++ .../response/BatchStopResponseImpl.java | 90 +++++++++++++++++++ .../response/BatchStopResponseDtoTest.java | 26 ++++++ .../response/BatchStopResponseDto.json | 3 + 12 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 examples/snippets/src/main/java/voice/v2/batches/Stop.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponseImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponseDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchStopResponseDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 980d251f7..22d54ab8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,11 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Calls API: `start` operation to create an outbound call - **[feature]** Voice V2 Batches API: `start` operation to create a batch of outbound calls - **[feature]** Voice V2 Batches API: `get` operation to retrieve a batch summary +- **[feature]** Voice V2 Batches API: `stop` operation to stop processing a batch of calls - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/batches/Start` snippet - **[doc]** New `voice/v2/batches/Get` snippet +- **[doc]** New `voice/v2/batches/Stop` snippet ## v2.2.0 - 2026-09-17 diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java index 839810308..b3d3ab870 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java @@ -23,6 +23,8 @@ import com.sinch.sdk.domains.voice.api.v2.BatchesService; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponseDtoTest; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummaryDtoTest; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; @@ -59,6 +61,9 @@ public class BatchesServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/batches/response/BatchSummaryDto.json") String jsonBatchSummaryDto; + @GivenTextResource("/domains/voice/v2/batches/response/BatchStopResponseDto.json") + String jsonBatchStopResponseDto; + @BeforeEach public void initMocks() { service = @@ -178,4 +183,57 @@ void getMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + + @Test + void stop() throws ApiException { + + String batchId = "01BX5ZZKBKACTAV9WEVGEMMVRC"; + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/batches/" + + URLPathUtils.encodePathSegment(batchId), + HttpMethod.DELETE, + Collections.emptyList(), + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(202, null, Collections.emptyMap(), jsonBatchStopResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + BatchStopResponse response = service.stop(batchId); + + TestHelpers.recursiveEquals(response, BatchStopResponseDtoTest.expectedBatchStopResponse); + } + + @Test + void stopMissingBatchIdThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.stop(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void stopMissingProjectIdThrows() { + + BatchesService serviceWithoutProjectId = + new BatchesServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, () -> serviceWithoutProjectId.stop("01BX5ZZKBKACTAV9WEVGEMMVRC")); + + Assertions.assertEquals(400, thrown.getCode()); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java index 9644e40f6..fadc9d3af 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java @@ -4,6 +4,7 @@ import com.sinch.sdk.domains.voice.api.v2.BatchesService; import com.sinch.sdk.domains.voice.models.v2.batches.request.BatchOptions; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; import com.sinch.sdk.domains.voice.models.v2.destination.Phone; @@ -27,6 +28,7 @@ public class BatchesSteps { BatchesService service; StartBatchResponse startResponse; BatchSummary getResponse; + BatchStopResponse stopResponse; @Given("^the Voice-V2 service \"Batches\" is available$") public void serviceAvailable() { @@ -111,4 +113,17 @@ public void getResult() { TestHelpers.recursiveEquals(getResponse, expected); } + + @When("^I send a request to stop batch processing$") + public void stop() { + stopResponse = service.stop("01M144V4N3GSTNVJ3V32TD7H9A"); + } + + @Then("^the response confirms the batch stop request was accepted$") + public void stopResult() { + BatchStopResponse expected = + BatchStopResponse.builder().setResult(BatchStopResponse.ResultEnum.STOP_REQUESTED).build(); + + TestHelpers.recursiveEquals(stopResponse, expected); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 6d718aa3d..545b10978 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -26,6 +26,7 @@ @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.sinch.sdk.e2e.domains.voice.v2") @ConfigurationParameter( key = FILTER_NAME_PROPERTY_NAME, - value = "^\\[Start\\].*|^\\[Get\\] get a batch call summary$") + value = + "^\\[Start\\].*|^\\[Get\\] get a batch call summary$|^\\[Stop\\] stop batch processing$") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index b3f509063..1dba9ac2b 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -28,3 +28,4 @@ See main [README.md](../../../../README.md) for how to execute snippets - Batches - [voice/v2/batches/Start](./v2/batches/Start.java) - [voice/v2/batches/Get](./v2/batches/Get.java) + - [voice/v2/batches/Stop](./v2/batches/Stop.java) diff --git a/examples/snippets/src/main/java/voice/v2/batches/Stop.java b/examples/snippets/src/main/java/voice/v2/batches/Stop.java new file mode 100644 index 000000000..e2df9855a --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/batches/Stop.java @@ -0,0 +1,47 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.batches; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.BatchesService; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class Stop { + + private static final Logger LOGGER = Logger.getLogger(Stop.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the batch to stop processing + String batchId = "BATCH_ID"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + BatchesService batchesService = client.voice().v2().batches(); + + LOGGER.info(String.format("Stop processing batch with ID '%s'", batchId)); + + BatchStopResponse response = batchesService.stop(batchId); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java index af37482f9..86e100989 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java @@ -3,6 +3,7 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; @@ -55,4 +56,17 @@ StartBatchResponse start( * @since 2.3 */ BatchSummary get(String batchId) throws ApiException; + + /** + * Stop processing a batch of call sessions + * + *

Prevents any queued calls in the batch from being initiated. Calls that are already in + * progress are not affected and continue until completion. + * + * @param batchId The ID of the batch, as returned by {@link #start} (required) + * @return BatchStopResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + BatchStopResponse stop(String batchId) throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java index aeedd968e..aefee9c5b 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java @@ -16,6 +16,7 @@ import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; import java.util.ArrayList; @@ -203,4 +204,68 @@ private HttpRequest getRequestBuilder(String batchId) throws ApiException { localVarContentTypes, localVarAuthNames); } + + @Override + public BatchStopResponse stop(String batchId) throws ApiException { + + LOGGER.finest("[stop]" + " " + "batchId: " + batchId); + + HttpRequest httpRequest = stopRequestBuilder(batchId); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest stopRequestBuilder(String batchId) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling stop"); + } + // verify the required parameter 'batchId' is set + if (batchId == null) { + throw new ApiException(400, "Missing the required parameter 'batchId' when calling stop"); + } + + String localVarPath = + "/v2/projects/{projectId}/batches/{batchId}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "batchId" + "\\}", URLPathUtils.encodePathSegment(batchId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.DELETE, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponse.java new file mode 100644 index 000000000..0546a2c5b --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponse.java @@ -0,0 +1,77 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Response returned when the request to stop processing a batch of call sessions has been received. + */ +@JsonDeserialize(builder = BatchStopResponseImpl.Builder.class) +public interface BatchStopResponse { + + /** State of the batch processing cancellation request */ + public class ResultEnum extends EnumDynamic { + public static final ResultEnum STOP_REQUESTED = new ResultEnum("STOP_REQUESTED"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(ResultEnum.class, ResultEnum::new, Arrays.asList(STOP_REQUESTED)); + + private ResultEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static ResultEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(ResultEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * State of the batch processing cancellation request. + * + *

STOP_REQUESTED: batch call processing cancellation requested. No new calls will + * be initiated. + * + * @return result + */ + ResultEnum getResult(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new BatchStopResponseImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param result see getter + * @return Current builder + * @see #getResult + */ + Builder setResult(ResultEnum result); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + BatchStopResponse build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponseImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponseImpl.java new file mode 100644 index 000000000..79253e50e --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponseImpl.java @@ -0,0 +1,90 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({BatchStopResponseImpl.JSON_PROPERTY_RESULT}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class BatchStopResponseImpl implements BatchStopResponse { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_RESULT = "result"; + + private OptionalValue result; + + public BatchStopResponseImpl() {} + + protected BatchStopResponseImpl(OptionalValue result) { + this.result = result; + } + + @JsonIgnore + public ResultEnum getResult() { + return result.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_RESULT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue result() { + return result; + } + + /** Return true if this BatchStopResponse object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BatchStopResponseImpl batchStopResponse = (BatchStopResponseImpl) o; + return Objects.equals(this.result, batchStopResponse.result); + } + + @Override + public int hashCode() { + return Objects.hash(result); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class BatchStopResponseImpl {\n"); + sb.append(" result: ").append(toIndentedString(result)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements BatchStopResponse.Builder { + OptionalValue result = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_RESULT) + public Builder setResult(ResultEnum result) { + this.result = OptionalValue.of(result); + return this; + } + + public BatchStopResponse build() { + return new BatchStopResponseImpl(result); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponseDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponseDtoTest.java new file mode 100644 index 000000000..bced282f5 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchStopResponseDtoTest.java @@ -0,0 +1,26 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class BatchStopResponseDtoTest extends BaseTest { + + public static final BatchStopResponse expectedBatchStopResponse = + BatchStopResponse.builder().setResult(BatchStopResponse.ResultEnum.STOP_REQUESTED).build(); + + @GivenTextResource("/domains/voice/v2/batches/response/BatchStopResponseDto.json") + String jsonBatchStopResponse; + + @Test + void deserialize() throws JsonProcessingException { + BatchStopResponse deserialized = + objectMapper.readValue(jsonBatchStopResponse, BatchStopResponse.class); + + TestHelpers.recursiveEquals(deserialized, expectedBatchStopResponse); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchStopResponseDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchStopResponseDto.json new file mode 100644 index 000000000..7b83e8207 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchStopResponseDto.json @@ -0,0 +1,3 @@ +{ + "result": "STOP_REQUESTED" +} From d61fd3b9c71a8de0bd943972b6e1893cda37cddf Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Tue, 6 Oct 2026 11:06:38 +0200 Subject: [PATCH 22/50] DEVEXP-1623: Add Voice v2 batches.getDetails endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/BatchesServiceTest.java | 61 +++++++++ .../e2e/domains/voice/v2/BatchesSteps.java | 28 +++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 3 +- .../snippets/src/main/java/voice/README.md | 1 + .../java/voice/v2/batches/GetDetails.java | 47 +++++++ .../domains/voice/api/v2/BatchesService.java | 18 +++ .../api/v2/adapters/BatchesServiceImpl.java | 66 ++++++++++ .../domains/voice/models/v2/SessionState.java | 50 ++++++++ .../v2/batches/response/BatchDetails.java | 52 ++++++++ .../v2/batches/response/BatchDetailsImpl.java | 91 ++++++++++++++ .../batches/response/BatchSessionSummary.java | 65 ++++++++++ .../response/BatchSessionSummaryImpl.java | 119 ++++++++++++++++++ .../batches/response/BatchDetailsDtoTest.java | 39 ++++++ .../response/BatchSessionSummaryDtoTest.java | 30 +++++ .../v2/batches/response/BatchDetailsDto.json | 16 +++ .../response/BatchSessionSummaryDto.json | 4 + 17 files changed, 691 insertions(+), 1 deletion(-) create mode 100644 examples/snippets/src/main/java/voice/v2/batches/GetDetails.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/SessionState.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetails.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummary.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchDetailsDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSessionSummaryDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d54ab8e..8c20e28d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,12 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Calls API: `start` operation to create an outbound call - **[feature]** Voice V2 Batches API: `start` operation to create a batch of outbound calls - **[feature]** Voice V2 Batches API: `get` operation to retrieve a batch summary +- **[feature]** Voice V2 Batches API: `getDetails` operation to retrieve the per-session details of a batch - **[feature]** Voice V2 Batches API: `stop` operation to stop processing a batch of calls - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/batches/Start` snippet - **[doc]** New `voice/v2/batches/Get` snippet +- **[doc]** New `voice/v2/batches/GetDetails` snippet - **[doc]** New `voice/v2/batches/Stop` snippet diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java index b3d3ab870..7a378a7f5 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java @@ -23,6 +23,8 @@ import com.sinch.sdk.domains.voice.api.v2.BatchesService; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchDetails; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchDetailsDtoTest; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponseDtoTest; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; @@ -61,6 +63,9 @@ public class BatchesServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/batches/response/BatchSummaryDto.json") String jsonBatchSummaryDto; + @GivenTextResource("/domains/voice/v2/batches/response/BatchDetailsDto.json") + String jsonBatchDetailsDto; + @GivenTextResource("/domains/voice/v2/batches/response/BatchStopResponseDto.json") String jsonBatchStopResponseDto; @@ -184,6 +189,62 @@ void getMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + @Test + void getDetails() throws ApiException { + + String batchId = "01BX5ZZKBKACTAV9WEVGEMMVRC"; + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/batches/" + + URLPathUtils.encodePathSegment(batchId) + + "/details", + HttpMethod.GET, + Collections.emptyList(), + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonBatchDetailsDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + BatchDetails response = service.getDetails(batchId); + + TestHelpers.recursiveEquals(response, BatchDetailsDtoTest.expectedBatchDetails); + } + + @Test + void getDetailsMissingBatchIdThrows() { + + ApiException thrown = + Assertions.assertThrows(ApiException.class, () -> service.getDetails(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void getDetailsMissingProjectIdThrows() { + + BatchesService serviceWithoutProjectId = + new BatchesServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> serviceWithoutProjectId.getDetails("01BX5ZZKBKACTAV9WEVGEMMVRC")); + + Assertions.assertEquals(400, thrown.getCode()); + } + @Test void stop() throws ApiException { diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java index fadc9d3af..b8bdf51bc 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java @@ -2,8 +2,11 @@ import com.sinch.sdk.core.TestHelpers; import com.sinch.sdk.domains.voice.api.v2.BatchesService; +import com.sinch.sdk.domains.voice.models.v2.SessionState; import com.sinch.sdk.domains.voice.models.v2.batches.request.BatchOptions; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchDetails; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSessionSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; @@ -28,6 +31,7 @@ public class BatchesSteps { BatchesService service; StartBatchResponse startResponse; BatchSummary getResponse; + BatchDetails getDetailsResponse; BatchStopResponse stopResponse; @Given("^the Voice-V2 service \"Batches\" is available$") @@ -114,6 +118,30 @@ public void getResult() { TestHelpers.recursiveEquals(getResponse, expected); } + @When("^I send a request to get batch call details$") + public void getDetails() { + getDetailsResponse = service.getDetails("01M144V4N3GSTNVJ3V32TD7H9A"); + } + + @Then("^the response contains the batch call details$") + public void getDetailsResult() { + BatchDetails expected = + BatchDetails.builder() + .setSessions( + Arrays.asList( + BatchSessionSummary.builder() + .setId("01M144V4PE5KTSVY2AX19QC332") + .setState(SessionState.IN_PROGRESS) + .build(), + BatchSessionSummary.builder() + .setId("01M144V4PEH22EHB1SHMJRXBXA") + .setState(SessionState.COMPLETED) + .build())) + .build(); + + TestHelpers.recursiveEquals(getDetailsResponse, expected); + } + @When("^I send a request to stop batch processing$") public void stop() { stopResponse = service.stop("01M144V4N3GSTNVJ3V32TD7H9A"); diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 545b10978..3ac8f8574 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -27,6 +27,7 @@ @ConfigurationParameter( key = FILTER_NAME_PROPERTY_NAME, value = - "^\\[Start\\].*|^\\[Get\\] get a batch call summary$|^\\[Stop\\] stop batch processing$") + "^\\[Start\\].*|^\\[Get\\] get a batch call summary$|^\\[GetDetails\\] get batch call" + + " details$|^\\[Stop\\] stop batch processing$") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 1dba9ac2b..2b9db9632 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -28,4 +28,5 @@ See main [README.md](../../../../README.md) for how to execute snippets - Batches - [voice/v2/batches/Start](./v2/batches/Start.java) - [voice/v2/batches/Get](./v2/batches/Get.java) + - [voice/v2/batches/GetDetails](./v2/batches/GetDetails.java) - [voice/v2/batches/Stop](./v2/batches/Stop.java) diff --git a/examples/snippets/src/main/java/voice/v2/batches/GetDetails.java b/examples/snippets/src/main/java/voice/v2/batches/GetDetails.java new file mode 100644 index 000000000..455e2782b --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/batches/GetDetails.java @@ -0,0 +1,47 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.batches; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.BatchesService; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchDetails; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class GetDetails { + + private static final Logger LOGGER = Logger.getLogger(GetDetails.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the batch to get details for + String batchId = "BATCH_ID"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + BatchesService batchesService = client.voice().v2().batches(); + + LOGGER.info(String.format("Get details for batch with ID '%s'", batchId)); + + BatchDetails response = batchesService.getDetails(batchId); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java index 86e100989..c474e840b 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java @@ -3,6 +3,7 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchDetails; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; @@ -57,6 +58,23 @@ StartBatchResponse start( */ BatchSummary get(String batchId) throws ApiException; + /** + * Get batch details + * + *

Retrieve per-session details for a batch call operation, including the current state of each + * call session in the batch. + * + *

Use this operation when individual session-level visibility is needed (for example, to + * inspect which sessions are QUEUED, IN_PROGRESS or COMPLETED + * ). EXPIRED sessions are never returned because they were never initiated. + * + * @param batchId The ID of the batch, as returned by {@link #start} (required) + * @return BatchDetails + * @throws ApiException if fails to make API call + * @since 2.3 + */ + BatchDetails getDetails(String batchId) throws ApiException; + /** * Stop processing a batch of call sessions * diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java index aefee9c5b..3285b2233 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java @@ -16,6 +16,7 @@ import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchQueryParameters; import com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest; +import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchDetails; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchStopResponse; import com.sinch.sdk.domains.voice.models.v2.batches.response.BatchSummary; import com.sinch.sdk.domains.voice.models.v2.batches.response.StartBatchResponse; @@ -205,6 +206,71 @@ private HttpRequest getRequestBuilder(String batchId) throws ApiException { localVarAuthNames); } + @Override + public BatchDetails getDetails(String batchId) throws ApiException { + + LOGGER.finest("[getDetails]" + " " + "batchId: " + batchId); + + HttpRequest httpRequest = getDetailsRequestBuilder(batchId); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest getDetailsRequestBuilder(String batchId) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling getDetails"); + } + // verify the required parameter 'batchId' is set + if (batchId == null) { + throw new ApiException( + 400, "Missing the required parameter 'batchId' when calling getDetails"); + } + + String localVarPath = + "/v2/projects/{projectId}/batches/{batchId}/details" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "batchId" + "\\}", URLPathUtils.encodePathSegment(batchId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.GET, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } + @Override public BatchStopResponse stop(String batchId) throws ApiException { diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/SessionState.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/SessionState.java new file mode 100644 index 000000000..144433387 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/SessionState.java @@ -0,0 +1,50 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * The state of the current session. Sessions can either be in a "transitional" or + * "final" state. Sessions in a transitional state are ongoing. Sessions in a final state + * will not send any more events. + */ +public class SessionState extends EnumDynamic { + + /** The session is queued, no calls have been initiated for this session. Transitional state. */ + public static final SessionState QUEUED = new SessionState("QUEUED"); + + /** At least one call has been initiated or received for this session. Transitional state. */ + public static final SessionState IN_PROGRESS = new SessionState("IN_PROGRESS"); + + /** The session was completed. Final state. */ + public static final SessionState COMPLETED = new SessionState("COMPLETED"); + + /** + * The call session was not initiated before the batch time-to-live (TTL) elapsed. Final state. + */ + public static final SessionState EXPIRED = new SessionState("EXPIRED"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + SessionState.class, + SessionState::new, + Arrays.asList(QUEUED, IN_PROGRESS, COMPLETED, EXPIRED)); + + private SessionState(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static SessionState from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(SessionState e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetails.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetails.java new file mode 100644 index 000000000..00a316764 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetails.java @@ -0,0 +1,52 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** + * Detailed per-session view of a batch call operation. Contains one entry for each call session in + * the batch, including the session identifier and its current execution state. + */ +@JsonDeserialize(builder = BatchDetailsImpl.Builder.class) +public interface BatchDetails { + + /** + * Per-session details for the batch. + * + *

EXPIRED sessions are never returned because they were never initiated. + * + *

Field is required + * + * @return sessions + */ + List getSessions(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new BatchDetailsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param sessions see getter + * @return Current builder + * @see #getSessions + */ + Builder setSessions(List sessions); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + BatchDetails build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsImpl.java new file mode 100644 index 000000000..ecb9bec4e --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsImpl.java @@ -0,0 +1,91 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({BatchDetailsImpl.JSON_PROPERTY_SESSIONS}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class BatchDetailsImpl implements BatchDetails { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_SESSIONS = "sessions"; + + private OptionalValue> sessions; + + public BatchDetailsImpl() {} + + protected BatchDetailsImpl(OptionalValue> sessions) { + this.sessions = sessions; + } + + @JsonIgnore + public List getSessions() { + return sessions.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SESSIONS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> sessions() { + return sessions; + } + + /** Return true if this BatchDetails object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BatchDetailsImpl batchDetails = (BatchDetailsImpl) o; + return Objects.equals(this.sessions, batchDetails.sessions); + } + + @Override + public int hashCode() { + return Objects.hash(sessions); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class BatchDetailsImpl {\n"); + sb.append(" sessions: ").append(toIndentedString(sessions)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements BatchDetails.Builder { + OptionalValue> sessions = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_SESSIONS, required = true) + public Builder setSessions(List sessions) { + this.sessions = OptionalValue.of(sessions); + return this; + } + + public BatchDetails build() { + return new BatchDetailsImpl(sessions); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummary.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummary.java new file mode 100644 index 000000000..cb57897a6 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummary.java @@ -0,0 +1,65 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.SessionState; + +/** Identifier and current execution state of a call session within a batch. */ +@JsonDeserialize(builder = BatchSessionSummaryImpl.Builder.class) +public interface BatchSessionSummary { + + /** + * Unique identifier of the call session within the batch (ULID). This identifies the session, not + * an individual call. + * + *

Use it with /v2/projects/{projectId}/sessions/{sessionId} to retrieve full + * session details. + * + * @return id + */ + String getId(); + + /** + * Current execution state of the call session. + * + * @return state + */ + SessionState getState(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new BatchSessionSummaryImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param id see getter + * @return Current builder + * @see #getId + */ + Builder setId(String id); + + /** + * see getter + * + * @param state see getter + * @return Current builder + * @see #getState + */ + Builder setState(SessionState state); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + BatchSessionSummary build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryImpl.java new file mode 100644 index 000000000..f6948032c --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryImpl.java @@ -0,0 +1,119 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.SessionState; +import java.util.Objects; + +@JsonPropertyOrder({ + BatchSessionSummaryImpl.JSON_PROPERTY_ID, + BatchSessionSummaryImpl.JSON_PROPERTY_STATE +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class BatchSessionSummaryImpl implements BatchSessionSummary { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ID = "id"; + + private OptionalValue id; + + public static final String JSON_PROPERTY_STATE = "state"; + + private OptionalValue state; + + public BatchSessionSummaryImpl() {} + + protected BatchSessionSummaryImpl(OptionalValue id, OptionalValue state) { + this.id = id; + this.state = state; + } + + @JsonIgnore + public String getId() { + return id.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ID) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue id() { + return id; + } + + @JsonIgnore + public SessionState getState() { + return state.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_STATE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue state() { + return state; + } + + /** Return true if this BatchSessionSummary object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BatchSessionSummaryImpl batchSessionSummary = (BatchSessionSummaryImpl) o; + return Objects.equals(this.id, batchSessionSummary.id) + && Objects.equals(this.state, batchSessionSummary.state); + } + + @Override + public int hashCode() { + return Objects.hash(id, state); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class BatchSessionSummaryImpl {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" state: ").append(toIndentedString(state)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements BatchSessionSummary.Builder { + OptionalValue id = OptionalValue.empty(); + OptionalValue state = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_ID) + public Builder setId(String id) { + this.id = OptionalValue.of(id); + return this; + } + + @JsonProperty(JSON_PROPERTY_STATE) + public Builder setState(SessionState state) { + this.state = OptionalValue.of(state); + return this; + } + + public BatchSessionSummary build() { + return new BatchSessionSummaryImpl(id, state); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsDtoTest.java new file mode 100644 index 000000000..bf4b28b9e --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsDtoTest.java @@ -0,0 +1,39 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.SessionState; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class BatchDetailsDtoTest extends BaseTest { + + public static final BatchDetails expectedBatchDetails = + BatchDetails.builder() + .setSessions( + Arrays.asList( + BatchSessionSummaryDtoTest.expectedBatchSessionSummary, + BatchSessionSummary.builder() + .setId("01F8Z5J4X2G9Y3J4X2G9Y3J4X5YJ") + .setState(SessionState.IN_PROGRESS) + .build(), + BatchSessionSummary.builder() + .setId("01F8Z5J4X2G9Y3J4X2G9Y3J4X9QK") + .setState(SessionState.QUEUED) + .build())) + .build(); + + @GivenTextResource("/domains/voice/v2/batches/response/BatchDetailsDto.json") + String jsonBatchDetails; + + @Test + void deserialize() throws JsonProcessingException { + BatchDetails deserialized = objectMapper.readValue(jsonBatchDetails, BatchDetails.class); + + TestHelpers.recursiveEquals(deserialized, expectedBatchDetails); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryDtoTest.java new file mode 100644 index 000000000..e757348ef --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryDtoTest.java @@ -0,0 +1,30 @@ +package com.sinch.sdk.domains.voice.models.v2.batches.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.SessionState; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class BatchSessionSummaryDtoTest extends BaseTest { + + public static final BatchSessionSummary expectedBatchSessionSummary = + BatchSessionSummary.builder() + .setId("01F8Z5J4X2G9Y3J4X2G9Y3J4X2G9") + .setState(SessionState.COMPLETED) + .build(); + + @GivenTextResource("/domains/voice/v2/batches/response/BatchSessionSummaryDto.json") + String jsonBatchSessionSummary; + + @Test + void deserialize() throws JsonProcessingException { + BatchSessionSummary deserialized = + objectMapper.readValue(jsonBatchSessionSummary, BatchSessionSummary.class); + + TestHelpers.recursiveEquals(deserialized, expectedBatchSessionSummary); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchDetailsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchDetailsDto.json new file mode 100644 index 000000000..9305d716b --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchDetailsDto.json @@ -0,0 +1,16 @@ +{ + "sessions": [ + { + "id": "01F8Z5J4X2G9Y3J4X2G9Y3J4X2G9", + "state": "COMPLETED" + }, + { + "id": "01F8Z5J4X2G9Y3J4X2G9Y3J4X5YJ", + "state": "IN_PROGRESS" + }, + { + "id": "01F8Z5J4X2G9Y3J4X2G9Y3J4X9QK", + "state": "QUEUED" + } + ] +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSessionSummaryDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSessionSummaryDto.json new file mode 100644 index 000000000..ded35743a --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSessionSummaryDto.json @@ -0,0 +1,4 @@ +{ + "id": "01F8Z5J4X2G9Y3J4X2G9Y3J4X2G9", + "state": "COMPLETED" +} From 605b1a855769a5b4f4527cfd82f8a02ac1df9588 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Tue, 6 Oct 2026 12:11:03 +0200 Subject: [PATCH 23/50] DEVEXP-1623: Add Voice v2 sessions.get endpoint --- CHANGELOG.md | 2 + .../domains/voice/api/v2/VoiceService.java | 8 + .../voice/api/v2/adapters/VoiceService.java | 20 + .../v2/sessions/response/package-info.java | 6 + .../api/v2/adapters/SessionsServiceTest.java | 110 +++ .../api/v2/adapters/VoiceServiceTest.java | 5 + .../e2e/domains/voice/v2/SessionsSteps.java | 78 ++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 9 +- .../snippets/src/main/java/voice/README.md | 2 + .../src/main/java/voice/v2/sessions/Get.java | 47 ++ .../domains/voice/api/v2/SessionsService.java | 22 + .../api/v2/adapters/SessionsServiceImpl.java | 111 +++ .../sdk/domains/voice/models/v2/Call.java | 400 +++++++++++ .../voice/models/v2/CallDirection.java | 36 + .../sdk/domains/voice/models/v2/CallImpl.java | 680 ++++++++++++++++++ .../domains/voice/models/v2/CallReason.java | 78 ++ .../domains/voice/models/v2/CallResult.java | 73 ++ .../sdk/domains/voice/models/v2/CallType.java | 47 ++ .../sdk/domains/voice/models/v2/Money.java | 66 ++ .../domains/voice/models/v2/MoneyImpl.java | 115 +++ .../voice/models/v2/OriginationType.java | 43 ++ .../models/v2/sessions/response/Session.java | 184 +++++ .../v2/sessions/response/SessionImpl.java | 288 ++++++++ .../domains/voice/models/v2/CallDtoTest.java | 77 ++ .../domains/voice/models/v2/MoneyDtoTest.java | 25 + .../v2/sessions/response/SessionDtoTest.java | 58 ++ .../resources/domains/voice/v2/CallDto.json | 37 + .../domains/voice/v2/CallMinimalDto.json | 16 + .../resources/domains/voice/v2/MoneyDto.json | 4 + .../v2/sessions/response/SessionDto.json | 48 ++ .../sessions/response/SessionMinimalDto.json | 25 + 31 files changed, 2716 insertions(+), 4 deletions(-) create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/package-info.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SessionsServiceTest.java create mode 100644 client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SessionsSteps.java create mode 100644 examples/snippets/src/main/java/voice/v2/sessions/Get.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SessionsService.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SessionsServiceImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/Call.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallDirection.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallReason.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallResult.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallType.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/Money.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/MoneyImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/OriginationType.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/Session.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/SessionImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/CallDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/MoneyDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sessions/response/SessionDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/CallDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/CallMinimalDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/MoneyDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/sessions/response/SessionDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/sessions/response/SessionMinimalDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c20e28d9..d09e6a480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,11 +24,13 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Batches API: `get` operation to retrieve a batch summary - **[feature]** Voice V2 Batches API: `getDetails` operation to retrieve the per-session details of a batch - **[feature]** Voice V2 Batches API: `stop` operation to stop processing a batch of calls +- **[feature]** Voice V2 Sessions API: `get` operation to retrieve a session and its calls - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/batches/Start` snippet - **[doc]** New `voice/v2/batches/Get` snippet - **[doc]** New `voice/v2/batches/GetDetails` snippet - **[doc]** New `voice/v2/batches/Stop` snippet +- **[doc]** New `voice/v2/sessions/Get` snippet ## v2.2.0 - 2026-09-17 diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java index e77afc979..cdc3f87b6 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java @@ -24,4 +24,12 @@ public interface VoiceService { * @since 2.3 */ BatchesService batches(); + + /** + * Sessions Service instance + * + * @return service instance for project + * @since 2.3 + */ + SessionsService sessions(); } diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java index 03cd7baba..ed3481b59 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java @@ -8,6 +8,7 @@ import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.domains.voice.api.v2.BatchesService; import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.api.v2.SessionsService; import com.sinch.sdk.domains.voice.models.v2.VoiceContext; import com.sinch.sdk.models.UnifiedCredentials; import java.util.AbstractMap; @@ -33,6 +34,7 @@ public class VoiceService implements com.sinch.sdk.domains.voice.api.v2.VoiceSer private volatile CallsService calls; private volatile BatchesService batches; + private volatile SessionsService sessions; public VoiceService( UnifiedCredentials credentials, @@ -81,6 +83,24 @@ public BatchesService batches() { return this.batches; } + public SessionsService sessions() { + if (null == this.sessions) { + synchronized (this) { + if (null == this.sessions) { + instanceLazyInit(); + this.sessions = + new SessionsServiceImpl( + httpClientSupplier.get(), + context.getVoiceServer(), + authManagers, + HttpMapper.getInstance(), + uriUUID); + } + } + } + return this.sessions; + } + private void instanceLazyInit() { if (null != this.authManagers) { return; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/package-info.java new file mode 100644 index 000000000..52444885a --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 sessions responses + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.sessions.response; diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SessionsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SessionsServiceTest.java new file mode 100644 index 000000000..e4aa51866 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SessionsServiceTest.java @@ -0,0 +1,110 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpContentType; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.HttpRequestTest.HttpRequestMatcher; +import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.URLPathUtils; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.domains.voice.api.v2.SessionsService; +import com.sinch.sdk.domains.voice.models.v2.sessions.response.Session; +import com.sinch.sdk.domains.voice.models.v2.sessions.response.SessionDtoTest; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +@TestWithResources +public class SessionsServiceTest extends BaseTest { + + @Mock HttpClient httpClient; + @Mock ServerConfiguration serverConfiguration; + @Mock Map authManagers; + + static final String PROJECT_ID = "test_project_id"; + static final Collection AUTH_NAMES = Arrays.asList("BasicAuth", "SinchOAuth2"); + static final Collection ACCEPTS = + Arrays.asList(HttpContentType.APPLICATION_JSON, "application/problem+json"); + + SessionsService service; + + @GivenTextResource("/domains/voice/v2/sessions/response/SessionDto.json") + String jsonSessionDto; + + @BeforeEach + public void initMocks() { + service = + new SessionsServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), PROJECT_ID); + } + + @Test + void get() throws ApiException { + + String sessionId = "01BX5ZZKBKACTAV9WEVGEMMVRB"; + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/sessions/" + + URLPathUtils.encodePathSegment(sessionId), + HttpMethod.GET, + Collections.emptyList(), + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonSessionDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + Session response = service.get(sessionId); + + TestHelpers.recursiveEquals(response, SessionDtoTest.expectedSession); + } + + @Test + void getMissingSessionIdThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.get(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void getMissingProjectIdThrows() { + + SessionsService serviceWithoutProjectId = + new SessionsServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, () -> serviceWithoutProjectId.get("01BX5ZZKBKACTAV9WEVGEMMVRB")); + + Assertions.assertEquals(400, thrown.getCode()); + } +} diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java index 98ce40fa1..b165ac667 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java @@ -17,4 +17,9 @@ void checkCredentialsCalls() { void checkCredentialsBatches() { CredentialsValidationHelper.checkCredentials(() -> httpClient, VoiceService::batches); } + + @Test + void checkCredentialsSessions() { + CredentialsValidationHelper.checkCredentials(() -> httpClient, VoiceService::sessions); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SessionsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SessionsSteps.java new file mode 100644 index 000000000..26a1e3b2f --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SessionsSteps.java @@ -0,0 +1,78 @@ +package com.sinch.sdk.e2e.domains.voice.v2; + +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.api.v2.SessionsService; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.CallDirection; +import com.sinch.sdk.domains.voice.models.v2.CallReason; +import com.sinch.sdk.domains.voice.models.v2.CallResult; +import com.sinch.sdk.domains.voice.models.v2.CallType; +import com.sinch.sdk.domains.voice.models.v2.Money; +import com.sinch.sdk.domains.voice.models.v2.OriginationType; +import com.sinch.sdk.domains.voice.models.v2.SessionState; +import com.sinch.sdk.domains.voice.models.v2.destination.Phone; +import com.sinch.sdk.domains.voice.models.v2.sessions.response.Session; +import com.sinch.sdk.e2e.Config; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.time.Instant; +import java.util.Collections; +import org.junit.jupiter.api.Assertions; + +public class SessionsSteps { + + SessionsService service; + Session getResponse; + + @Given("^the Voice-V2 service \"Sessions\" is available$") + public void serviceAvailable() { + service = Config.getSinchClient().voice().v2().sessions(); + Assertions.assertNotNull(service, "Voice V2 Sessions service is not available"); + } + + @When("^I send a request to get a session$") + public void get() { + getResponse = service.get("01J7K3X9M2P5R8V0W4Y6Z1A3B5"); + } + + @Then("^the response contains the session details$") + public void getResult() { + Call call = + Call.builder() + .setCallId("01J7K3Y2N4Q6S9W1X5Z7A2B4C6") + .setServiceId("e52d19b4-03fa-4e89-a901-d78b12f6a9e2") + .setProjectId("a8f3b91c-4e2d-4190-883a-71b5c92e31d4") + .setSessionId("01J7K3X9M2P5R8V0W4Y6Z1A3B5") + .setBatchId("01J7K3Z5P6R8T0X2Y7A9B3C5D7") + .setFrom(Phone.builder().setNumber("+12015555555").build()) + .setTo(Phone.builder().setNumber("+12017777777").build()) + .setDirection(CallDirection.OUTBOUND) + .setCallResult(CallResult.NO_ANSWER) + .setCallType(CallType.PHONE) + .setOriginationType(OriginationType.SERVER) + .setStartTime(Instant.parse("2026-08-28T12:17:18Z")) + .setEndTime(Instant.parse("2026-08-28T12:17:49Z")) + .setUpdateTime(Instant.parse("2026-08-28T12:17:49Z")) + .setCallDurationSeconds(0) + .setCallRate(Money.builder().setCurrencyCode("EUR").setAmount("0.4085").build()) + .setCallReason(CallReason.NOT_AVAILABLE) + .setCallResourceUrl( + "https://eu1.voice.api.sinch.com/v2/projects/a8f3b91c-4e2d-4190-883a-71b5c92e31d4/calls/01J7K3Y2N4Q6S9W1X5Z7A2B4C6") + .build(); + + Session expected = + Session.builder() + .setSessionId("01J7K3X9M2P5R8V0W4Y6Z1A3B5") + .setServiceId("e52d19b4-03fa-4e89-a901-d78b12f6a9e2") + .setProjectId("a8f3b91c-4e2d-4190-883a-71b5c92e31d4") + .setCalls(Collections.singletonList(call)) + .setState(SessionState.COMPLETED) + .setCreateTime(Instant.parse("2026-08-28T12:17:18Z")) + .setEndTime(Instant.parse("2026-08-28T12:17:49Z")) + .setUpdateTime(Instant.parse("2026-08-28T12:17:49Z")) + .build(); + + TestHelpers.recursiveEquals(getResponse, expected); + } +} diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 3ac8f8574..b9d2459da 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -14,20 +14,21 @@ * Only the scenarios of the operations this SDK implements. Widen the resource and the name filter * as the other Voice V2 operations land. * - *

The filter matches scenario names across both features, and both use the same {@code [Get]} - * prefix. Batches' {@code [Get]} is therefore matched by its full name, so that the calls {@code - * [Get]} scenario (not implemented yet) stays out. + *

The filter matches scenario names across all features, and they share the same {@code [Get]} + * prefix. Batches' and sessions' {@code [Get]} are therefore matched by their full name, so that + * the calls {@code [Get]} scenario (not implemented yet) stays out. */ @Suite @SuiteDisplayName("Voice V2") @IncludeEngines("cucumber") @SelectClasspathResource("features/voice-v2/calls.feature") @SelectClasspathResource("features/voice-v2/batches.feature") +@SelectClasspathResource("features/voice-v2/sessions.feature") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.sinch.sdk.e2e.domains.voice.v2") @ConfigurationParameter( key = FILTER_NAME_PROPERTY_NAME, value = "^\\[Start\\].*|^\\[Get\\] get a batch call summary$|^\\[GetDetails\\] get batch call" - + " details$|^\\[Stop\\] stop batch processing$") + + " details$|^\\[Stop\\] stop batch processing$|^\\[Get\\] get a session$") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 2b9db9632..beaf4c34f 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -30,3 +30,5 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/batches/Get](./v2/batches/Get.java) - [voice/v2/batches/GetDetails](./v2/batches/GetDetails.java) - [voice/v2/batches/Stop](./v2/batches/Stop.java) + - Sessions + - [voice/v2/sessions/Get](./v2/sessions/Get.java) diff --git a/examples/snippets/src/main/java/voice/v2/sessions/Get.java b/examples/snippets/src/main/java/voice/v2/sessions/Get.java new file mode 100644 index 000000000..c783f1992 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/sessions/Get.java @@ -0,0 +1,47 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.sessions; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.SessionsService; +import com.sinch.sdk.domains.voice.models.v2.sessions.response.Session; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class Get { + + private static final Logger LOGGER = Logger.getLogger(Get.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the session to retrieve + String sessionId = "SESSION_ID"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + SessionsService sessionsService = client.voice().v2().sessions(); + + LOGGER.info(String.format("Get session with ID '%s'", sessionId)); + + Session response = sessionsService.get(sessionId); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SessionsService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SessionsService.java new file mode 100644 index 000000000..82aaa6acf --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SessionsService.java @@ -0,0 +1,22 @@ +package com.sinch.sdk.domains.voice.api.v2; + +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.domains.voice.models.v2.sessions.response.Session; + +/** Sessions Service */ +public interface SessionsService { + + /** + * Get a session details by the session ID + * + *

Retrieve detailed information about a specific session, including all associated calls and + * their current states. Sessions represent the complete interaction lifecycle and can contain + * multiple related calls. + * + * @param sessionId The ID of the session (required) + * @return Session + * @throws ApiException if fails to make API call + * @since 2.3 + */ + Session get(String sessionId) throws ApiException; +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SessionsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SessionsServiceImpl.java new file mode 100644 index 000000000..2944c43f9 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SessionsServiceImpl.java @@ -0,0 +1,111 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.exceptions.ApiExceptionBuilder; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.HttpStatus; +import com.sinch.sdk.core.http.URLParameter; +import com.sinch.sdk.core.http.URLPathUtils; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.domains.voice.models.v2.sessions.response.Session; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +public class SessionsServiceImpl implements com.sinch.sdk.domains.voice.api.v2.SessionsService { + + private static final Logger LOGGER = Logger.getLogger(SessionsServiceImpl.class.getName()); + private final HttpClient httpClient; + private final ServerConfiguration serverConfiguration; + private final Map authManagersByOasSecuritySchemes; + private final HttpMapper mapper; + + private final String projectId; + + public SessionsServiceImpl( + HttpClient httpClient, + ServerConfiguration serverConfiguration, + Map authManagersByOasSecuritySchemes, + HttpMapper mapper, + String projectId) { + this.httpClient = httpClient; + this.serverConfiguration = serverConfiguration; + this.authManagersByOasSecuritySchemes = authManagersByOasSecuritySchemes; + this.mapper = mapper; + this.projectId = projectId; + } + + @Override + public Session get(String sessionId) throws ApiException { + + LOGGER.finest("[get]" + " " + "sessionId: " + sessionId); + + HttpRequest httpRequest = getRequestBuilder(sessionId); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest getRequestBuilder(String sessionId) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling get"); + } + // verify the required parameter 'sessionId' is set + if (sessionId == null) { + throw new ApiException(400, "Missing the required parameter 'sessionId' when calling get"); + } + + String localVarPath = + "/v2/projects/{projectId}/sessions/{sessionId}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "sessionId" + "\\}", URLPathUtils.encodePathSegment(sessionId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.GET, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/Call.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/Call.java new file mode 100644 index 000000000..38968a3d6 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/Call.java @@ -0,0 +1,400 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination; +import com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin; +import java.time.Instant; + +/** Call details */ +@JsonDeserialize(builder = CallImpl.Builder.class) +public interface Call { + + /** + * The ID of the call. + * + *

Field is required + * + * @return callId + */ + String getCallId(); + + /** + * The ID of the project associated with the call. + * + *

Field is required + * + * @return projectId + */ + String getProjectId(); + + /** + * The ID of the service used. + * + *

Field is required + * + * @return serviceId + */ + String getServiceId(); + + /** + * The ID of the session. + * + *

Field is required + * + * @return sessionId + */ + String getSessionId(); + + /** + * The name identifying this call leg within the session, as assigned by the callName + * property in the dial command or in the SVAML response to an incoming call webhook. + * + *

Omitted for calls that were not assigned a name. + * + * @return callName + */ + String getCallName(); + + /** + * The name of the bridge the call belongs to. Omitted for calls not assigned to any bridge. + * + * @return bridgeName + */ + String getBridgeName(); + + /** + * The ID of the batch. + * + * @return batchId + */ + String getBatchId(); + + /** + * Call origin - Phone Number or SIP endpoint + * + * @return from + */ + CallOrigin getFrom(); + + /** + * Call destination - Phone Number or Stream URI + * + * @return to + */ + CallDestination getTo(); + + /** + * Timestamp (RFC 3339) indicating when the call was created and call setup was initiated (start + * of the call attempt). + * + *

Field is required + * + * @return startTime + */ + Instant getStartTime(); + + /** + * Timestamp (RFC 3339) indicating when the call was last updated. + * + *

Omitted if no updates were performed on this call. + * + * @return updateTime + */ + Instant getUpdateTime(); + + /** + * The type of channel used for the call. + * + *

Field is required + * + * @return callType + */ + CallType getCallType(); + + /** + * Indicates the direction of the call. + * + *

Field is required + * + * @return direction + */ + CallDirection getDirection(); + + /** + * Timestamp (RFC 3339) indicating when the call was answered. + * + *

Omitted if the call was not answered. + * + * @return answerTime + */ + Instant getAnswerTime(); + + /** + * Timestamp (RFC 3339) indicating when the call ended. + * + *

Omitted for ongoing calls. + * + * @return endTime + */ + Instant getEndTime(); + + /** + * Duration of the call in seconds + * + * @return callDurationSeconds + */ + Integer getCallDurationSeconds(); + + /** + * The outcome/state of the call. + * + *

Field is required + * + * @return callResult + */ + CallResult getCallResult(); + + /** + * Reason explaining why the call ended in the given {@link #getCallResult() callResult}. + * + * @return callReason + */ + CallReason getCallReason(); + + /** + * Indicates the origin/source of the call. + * + *

Field is required + * + * @return originationType + */ + OriginationType getOriginationType(); + + /** + * The rate charged for this call, expressed as a monetary amount per minute in the + * specified currency. + * + *

Field is required + * + * @return callRate + */ + Money getCallRate(); + + /** + * Absolute URI to this call resource. Use this URL to retrieve the call details + * + *

Field is required + * + * @return callResourceUrl + */ + String getCallResourceUrl(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new CallImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param callId see getter + * @return Current builder + * @see #getCallId + */ + Builder setCallId(String callId); + + /** + * see getter + * + * @param projectId see getter + * @return Current builder + * @see #getProjectId + */ + Builder setProjectId(String projectId); + + /** + * see getter + * + * @param serviceId see getter + * @return Current builder + * @see #getServiceId + */ + Builder setServiceId(String serviceId); + + /** + * see getter + * + * @param sessionId see getter + * @return Current builder + * @see #getSessionId + */ + Builder setSessionId(String sessionId); + + /** + * see getter + * + * @param callName see getter + * @return Current builder + * @see #getCallName + */ + Builder setCallName(String callName); + + /** + * see getter + * + * @param bridgeName see getter + * @return Current builder + * @see #getBridgeName + */ + Builder setBridgeName(String bridgeName); + + /** + * see getter + * + * @param batchId see getter + * @return Current builder + * @see #getBatchId + */ + Builder setBatchId(String batchId); + + /** + * see getter + * + * @param from see getter + * @return Current builder + * @see #getFrom + */ + Builder setFrom(CallOrigin from); + + /** + * see getter + * + * @param to see getter + * @return Current builder + * @see #getTo + */ + Builder setTo(CallDestination to); + + /** + * see getter + * + * @param startTime see getter + * @return Current builder + * @see #getStartTime + */ + Builder setStartTime(Instant startTime); + + /** + * see getter + * + * @param updateTime see getter + * @return Current builder + * @see #getUpdateTime + */ + Builder setUpdateTime(Instant updateTime); + + /** + * see getter + * + * @param callType see getter + * @return Current builder + * @see #getCallType + */ + Builder setCallType(CallType callType); + + /** + * see getter + * + * @param direction see getter + * @return Current builder + * @see #getDirection + */ + Builder setDirection(CallDirection direction); + + /** + * see getter + * + * @param answerTime see getter + * @return Current builder + * @see #getAnswerTime + */ + Builder setAnswerTime(Instant answerTime); + + /** + * see getter + * + * @param endTime see getter + * @return Current builder + * @see #getEndTime + */ + Builder setEndTime(Instant endTime); + + /** + * see getter + * + * @param callDurationSeconds see getter + * @return Current builder + * @see #getCallDurationSeconds + */ + Builder setCallDurationSeconds(Integer callDurationSeconds); + + /** + * see getter + * + * @param callResult see getter + * @return Current builder + * @see #getCallResult + */ + Builder setCallResult(CallResult callResult); + + /** + * see getter + * + * @param callReason see getter + * @return Current builder + * @see #getCallReason + */ + Builder setCallReason(CallReason callReason); + + /** + * see getter + * + * @param originationType see getter + * @return Current builder + * @see #getOriginationType + */ + Builder setOriginationType(OriginationType originationType); + + /** + * see getter + * + * @param callRate see getter + * @return Current builder + * @see #getCallRate + */ + Builder setCallRate(Money callRate); + + /** + * see getter + * + * @param callResourceUrl see getter + * @return Current builder + * @see #getCallResourceUrl + */ + Builder setCallResourceUrl(String callResourceUrl); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + Call build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallDirection.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallDirection.java new file mode 100644 index 000000000..aa1ebfac9 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallDirection.java @@ -0,0 +1,36 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** Indicates the direction of the call. */ +public class CallDirection extends EnumDynamic { + + /** A call initiated towards the Sinch Calling Platform. */ + public static final CallDirection INBOUND = new CallDirection("INBOUND"); + + /** A call initiated from the Sinch Calling Platform. */ + public static final CallDirection OUTBOUND = new CallDirection("OUTBOUND"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + CallDirection.class, CallDirection::new, Arrays.asList(INBOUND, OUTBOUND)); + + private CallDirection(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CallDirection from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CallDirection e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallImpl.java new file mode 100644 index 000000000..a21b30934 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallImpl.java @@ -0,0 +1,680 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.destination.CallDestination; +import com.sinch.sdk.domains.voice.models.v2.destination.CallOrigin; +import java.time.Instant; +import java.util.Objects; + +@JsonPropertyOrder({ + CallImpl.JSON_PROPERTY_CALL_ID, + CallImpl.JSON_PROPERTY_PROJECT_ID, + CallImpl.JSON_PROPERTY_SERVICE_ID, + CallImpl.JSON_PROPERTY_SESSION_ID, + CallImpl.JSON_PROPERTY_CALL_NAME, + CallImpl.JSON_PROPERTY_BRIDGE_NAME, + CallImpl.JSON_PROPERTY_BATCH_ID, + CallImpl.JSON_PROPERTY_FROM, + CallImpl.JSON_PROPERTY_TO, + CallImpl.JSON_PROPERTY_START_TIME, + CallImpl.JSON_PROPERTY_UPDATE_TIME, + CallImpl.JSON_PROPERTY_CALL_TYPE, + CallImpl.JSON_PROPERTY_DIRECTION, + CallImpl.JSON_PROPERTY_ANSWER_TIME, + CallImpl.JSON_PROPERTY_END_TIME, + CallImpl.JSON_PROPERTY_CALL_DURATION_SECONDS, + CallImpl.JSON_PROPERTY_CALL_RESULT, + CallImpl.JSON_PROPERTY_CALL_REASON, + CallImpl.JSON_PROPERTY_ORIGINATION_TYPE, + CallImpl.JSON_PROPERTY_CALL_RATE, + CallImpl.JSON_PROPERTY_CALL_RESOURCE_URL +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class CallImpl implements Call { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_CALL_ID = "callId"; + + private OptionalValue callId; + + public static final String JSON_PROPERTY_PROJECT_ID = "projectId"; + + private OptionalValue projectId; + + public static final String JSON_PROPERTY_SERVICE_ID = "serviceId"; + + private OptionalValue serviceId; + + public static final String JSON_PROPERTY_SESSION_ID = "sessionId"; + + private OptionalValue sessionId; + + public static final String JSON_PROPERTY_CALL_NAME = "callName"; + + private OptionalValue callName; + + public static final String JSON_PROPERTY_BRIDGE_NAME = "bridgeName"; + + private OptionalValue bridgeName; + + public static final String JSON_PROPERTY_BATCH_ID = "batchId"; + + private OptionalValue batchId; + + public static final String JSON_PROPERTY_FROM = "from"; + + private OptionalValue from; + + public static final String JSON_PROPERTY_TO = "to"; + + private OptionalValue to; + + public static final String JSON_PROPERTY_START_TIME = "startTime"; + + private OptionalValue startTime; + + public static final String JSON_PROPERTY_UPDATE_TIME = "updateTime"; + + private OptionalValue updateTime; + + public static final String JSON_PROPERTY_CALL_TYPE = "callType"; + + private OptionalValue callType; + + public static final String JSON_PROPERTY_DIRECTION = "direction"; + + private OptionalValue direction; + + public static final String JSON_PROPERTY_ANSWER_TIME = "answerTime"; + + private OptionalValue answerTime; + + public static final String JSON_PROPERTY_END_TIME = "endTime"; + + private OptionalValue endTime; + + public static final String JSON_PROPERTY_CALL_DURATION_SECONDS = "callDurationSeconds"; + + private OptionalValue callDurationSeconds; + + public static final String JSON_PROPERTY_CALL_RESULT = "callResult"; + + private OptionalValue callResult; + + public static final String JSON_PROPERTY_CALL_REASON = "callReason"; + + private OptionalValue callReason; + + public static final String JSON_PROPERTY_ORIGINATION_TYPE = "originationType"; + + private OptionalValue originationType; + + public static final String JSON_PROPERTY_CALL_RATE = "callRate"; + + private OptionalValue callRate; + + public static final String JSON_PROPERTY_CALL_RESOURCE_URL = "callResourceUrl"; + + private OptionalValue callResourceUrl; + + public CallImpl() {} + + protected CallImpl( + OptionalValue callId, + OptionalValue projectId, + OptionalValue serviceId, + OptionalValue sessionId, + OptionalValue callName, + OptionalValue bridgeName, + OptionalValue batchId, + OptionalValue from, + OptionalValue to, + OptionalValue startTime, + OptionalValue updateTime, + OptionalValue callType, + OptionalValue direction, + OptionalValue answerTime, + OptionalValue endTime, + OptionalValue callDurationSeconds, + OptionalValue callResult, + OptionalValue callReason, + OptionalValue originationType, + OptionalValue callRate, + OptionalValue callResourceUrl) { + this.callId = callId; + this.projectId = projectId; + this.serviceId = serviceId; + this.sessionId = sessionId; + this.callName = callName; + this.bridgeName = bridgeName; + this.batchId = batchId; + this.from = from; + this.to = to; + this.startTime = startTime; + this.updateTime = updateTime; + this.callType = callType; + this.direction = direction; + this.answerTime = answerTime; + this.endTime = endTime; + this.callDurationSeconds = callDurationSeconds; + this.callResult = callResult; + this.callReason = callReason; + this.originationType = originationType; + this.callRate = callRate; + this.callResourceUrl = callResourceUrl; + } + + @JsonIgnore + public String getCallId() { + return callId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue callId() { + return callId; + } + + @JsonIgnore + public String getProjectId() { + return projectId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PROJECT_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue projectId() { + return projectId; + } + + @JsonIgnore + public String getServiceId() { + return serviceId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SERVICE_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue serviceId() { + return serviceId; + } + + @JsonIgnore + public String getSessionId() { + return sessionId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SESSION_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue sessionId() { + return sessionId; + } + + @JsonIgnore + public String getCallName() { + return callName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callName() { + return callName; + } + + @JsonIgnore + public String getBridgeName() { + return bridgeName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_BRIDGE_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue bridgeName() { + return bridgeName; + } + + @JsonIgnore + public String getBatchId() { + return batchId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_BATCH_ID) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue batchId() { + return batchId; + } + + @JsonIgnore + public CallOrigin getFrom() { + return from.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_FROM) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue from() { + return from; + } + + @JsonIgnore + public CallDestination getTo() { + return to.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TO) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue to() { + return to; + } + + @JsonIgnore + public Instant getStartTime() { + return startTime.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_START_TIME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue startTime() { + return startTime; + } + + @JsonIgnore + public Instant getUpdateTime() { + return updateTime.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_UPDATE_TIME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue updateTime() { + return updateTime; + } + + @JsonIgnore + public CallType getCallType() { + return callType.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue callType() { + return callType; + } + + @JsonIgnore + public CallDirection getDirection() { + return direction.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DIRECTION) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue direction() { + return direction; + } + + @JsonIgnore + public Instant getAnswerTime() { + return answerTime.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ANSWER_TIME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue answerTime() { + return answerTime; + } + + @JsonIgnore + public Instant getEndTime() { + return endTime.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_END_TIME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue endTime() { + return endTime; + } + + @JsonIgnore + public Integer getCallDurationSeconds() { + return callDurationSeconds.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_DURATION_SECONDS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callDurationSeconds() { + return callDurationSeconds; + } + + @JsonIgnore + public CallResult getCallResult() { + return callResult.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_RESULT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue callResult() { + return callResult; + } + + @JsonIgnore + public CallReason getCallReason() { + return callReason.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_REASON) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callReason() { + return callReason; + } + + @JsonIgnore + public OriginationType getOriginationType() { + return originationType.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ORIGINATION_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue originationType() { + return originationType; + } + + @JsonIgnore + public Money getCallRate() { + return callRate.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_RATE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue callRate() { + return callRate; + } + + @JsonIgnore + public String getCallResourceUrl() { + return callResourceUrl.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_RESOURCE_URL) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue callResourceUrl() { + return callResourceUrl; + } + + /** Return true if this Call object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CallImpl call = (CallImpl) o; + return Objects.equals(this.callId, call.callId) + && Objects.equals(this.projectId, call.projectId) + && Objects.equals(this.serviceId, call.serviceId) + && Objects.equals(this.sessionId, call.sessionId) + && Objects.equals(this.callName, call.callName) + && Objects.equals(this.bridgeName, call.bridgeName) + && Objects.equals(this.batchId, call.batchId) + && Objects.equals(this.from, call.from) + && Objects.equals(this.to, call.to) + && Objects.equals(this.startTime, call.startTime) + && Objects.equals(this.updateTime, call.updateTime) + && Objects.equals(this.callType, call.callType) + && Objects.equals(this.direction, call.direction) + && Objects.equals(this.answerTime, call.answerTime) + && Objects.equals(this.endTime, call.endTime) + && Objects.equals(this.callDurationSeconds, call.callDurationSeconds) + && Objects.equals(this.callResult, call.callResult) + && Objects.equals(this.callReason, call.callReason) + && Objects.equals(this.originationType, call.originationType) + && Objects.equals(this.callRate, call.callRate) + && Objects.equals(this.callResourceUrl, call.callResourceUrl); + } + + @Override + public int hashCode() { + return Objects.hash( + callId, + projectId, + serviceId, + sessionId, + callName, + bridgeName, + batchId, + from, + to, + startTime, + updateTime, + callType, + direction, + answerTime, + endTime, + callDurationSeconds, + callResult, + callReason, + originationType, + callRate, + callResourceUrl); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CallImpl {\n"); + sb.append(" callId: ").append(toIndentedString(callId)).append("\n"); + sb.append(" projectId: ").append(toIndentedString(projectId)).append("\n"); + sb.append(" serviceId: ").append(toIndentedString(serviceId)).append("\n"); + sb.append(" sessionId: ").append(toIndentedString(sessionId)).append("\n"); + sb.append(" callName: ").append(toIndentedString(callName)).append("\n"); + sb.append(" bridgeName: ").append(toIndentedString(bridgeName)).append("\n"); + sb.append(" batchId: ").append(toIndentedString(batchId)).append("\n"); + sb.append(" from: ").append(toIndentedString(from)).append("\n"); + sb.append(" to: ").append(toIndentedString(to)).append("\n"); + sb.append(" startTime: ").append(toIndentedString(startTime)).append("\n"); + sb.append(" updateTime: ").append(toIndentedString(updateTime)).append("\n"); + sb.append(" callType: ").append(toIndentedString(callType)).append("\n"); + sb.append(" direction: ").append(toIndentedString(direction)).append("\n"); + sb.append(" answerTime: ").append(toIndentedString(answerTime)).append("\n"); + sb.append(" endTime: ").append(toIndentedString(endTime)).append("\n"); + sb.append(" callDurationSeconds: ") + .append(toIndentedString(callDurationSeconds)) + .append("\n"); + sb.append(" callResult: ").append(toIndentedString(callResult)).append("\n"); + sb.append(" callReason: ").append(toIndentedString(callReason)).append("\n"); + sb.append(" originationType: ").append(toIndentedString(originationType)).append("\n"); + sb.append(" callRate: ").append(toIndentedString(callRate)).append("\n"); + sb.append(" callResourceUrl: ").append(toIndentedString(callResourceUrl)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements Call.Builder { + OptionalValue callId = OptionalValue.empty(); + OptionalValue projectId = OptionalValue.empty(); + OptionalValue serviceId = OptionalValue.empty(); + OptionalValue sessionId = OptionalValue.empty(); + OptionalValue callName = OptionalValue.empty(); + OptionalValue bridgeName = OptionalValue.empty(); + OptionalValue batchId = OptionalValue.empty(); + OptionalValue from = OptionalValue.empty(); + OptionalValue to = OptionalValue.empty(); + OptionalValue startTime = OptionalValue.empty(); + OptionalValue updateTime = OptionalValue.empty(); + OptionalValue callType = OptionalValue.empty(); + OptionalValue direction = OptionalValue.empty(); + OptionalValue answerTime = OptionalValue.empty(); + OptionalValue endTime = OptionalValue.empty(); + OptionalValue callDurationSeconds = OptionalValue.empty(); + OptionalValue callResult = OptionalValue.empty(); + OptionalValue callReason = OptionalValue.empty(); + OptionalValue originationType = OptionalValue.empty(); + OptionalValue callRate = OptionalValue.empty(); + OptionalValue callResourceUrl = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_CALL_ID, required = true) + public Builder setCallId(String callId) { + this.callId = OptionalValue.of(callId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_PROJECT_ID, required = true) + public Builder setProjectId(String projectId) { + this.projectId = OptionalValue.of(projectId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SERVICE_ID, required = true) + public Builder setServiceId(String serviceId) { + this.serviceId = OptionalValue.of(serviceId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SESSION_ID, required = true) + public Builder setSessionId(String sessionId) { + this.sessionId = OptionalValue.of(sessionId); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + public Builder setCallName(String callName) { + this.callName = OptionalValue.of(callName); + return this; + } + + @JsonProperty(JSON_PROPERTY_BRIDGE_NAME) + public Builder setBridgeName(String bridgeName) { + this.bridgeName = OptionalValue.of(bridgeName); + return this; + } + + @JsonProperty(JSON_PROPERTY_BATCH_ID) + public Builder setBatchId(String batchId) { + this.batchId = OptionalValue.of(batchId); + return this; + } + + @JsonProperty(JSON_PROPERTY_FROM) + public Builder setFrom(CallOrigin from) { + this.from = OptionalValue.of(from); + return this; + } + + @JsonProperty(JSON_PROPERTY_TO) + public Builder setTo(CallDestination to) { + this.to = OptionalValue.of(to); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_START_TIME, required = true) + public Builder setStartTime(Instant startTime) { + this.startTime = OptionalValue.of(startTime); + return this; + } + + @JsonProperty(JSON_PROPERTY_UPDATE_TIME) + public Builder setUpdateTime(Instant updateTime) { + this.updateTime = OptionalValue.of(updateTime); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CALL_TYPE, required = true) + public Builder setCallType(CallType callType) { + this.callType = OptionalValue.of(callType); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_DIRECTION, required = true) + public Builder setDirection(CallDirection direction) { + this.direction = OptionalValue.of(direction); + return this; + } + + @JsonProperty(JSON_PROPERTY_ANSWER_TIME) + public Builder setAnswerTime(Instant answerTime) { + this.answerTime = OptionalValue.of(answerTime); + return this; + } + + @JsonProperty(JSON_PROPERTY_END_TIME) + public Builder setEndTime(Instant endTime) { + this.endTime = OptionalValue.of(endTime); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_DURATION_SECONDS) + public Builder setCallDurationSeconds(Integer callDurationSeconds) { + this.callDurationSeconds = OptionalValue.of(callDurationSeconds); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CALL_RESULT, required = true) + public Builder setCallResult(CallResult callResult) { + this.callResult = OptionalValue.of(callResult); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_REASON) + public Builder setCallReason(CallReason callReason) { + this.callReason = OptionalValue.of(callReason); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_ORIGINATION_TYPE, required = true) + public Builder setOriginationType(OriginationType originationType) { + this.originationType = OptionalValue.of(originationType); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CALL_RATE, required = true) + public Builder setCallRate(Money callRate) { + this.callRate = OptionalValue.of(callRate); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CALL_RESOURCE_URL, required = true) + public Builder setCallResourceUrl(String callResourceUrl) { + this.callResourceUrl = OptionalValue.of(callResourceUrl); + return this; + } + + public Call build() { + return new CallImpl( + callId, + projectId, + serviceId, + sessionId, + callName, + bridgeName, + batchId, + from, + to, + startTime, + updateTime, + callType, + direction, + answerTime, + endTime, + callDurationSeconds, + callResult, + callReason, + originationType, + callRate, + callResourceUrl); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallReason.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallReason.java new file mode 100644 index 000000000..3c44328d8 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallReason.java @@ -0,0 +1,78 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Reason explaining why the call ended in the given {@link CallResult}. + * + *

{@link CallResult} describes what happened (state/outcome). The reason provides + * additional context about the underlying cause (for example, who terminated the call, routing + * issues, or validation errors). + */ +public class CallReason extends EnumDynamic { + + /** The call was completed successfully. */ + public static final CallReason OK = new CallReason("OK"); + + /** The callee was not available. */ + public static final CallReason NOT_AVAILABLE = new CallReason("NOT_AVAILABLE"); + + /** The caller hung up the call. */ + public static final CallReason CALLER_HANGUP = new CallReason("CALLER_HANGUP"); + + /** The callee hung up the call. */ + public static final CallReason CALLEE_HANGUP = new CallReason("CALLEE_HANGUP"); + + /** The call was ended by the call manager. */ + public static final CallReason MANAGER_HANGUP = new CallReason("MANAGER_HANGUP"); + + /** The DID number was not found. */ + public static final CallReason DID_NOT_FOUND = new CallReason("DID_NOT_FOUND"); + + /** The SVAML script was invalid. */ + public static final CallReason INVALID_SCRIPT = new CallReason("INVALID_SCRIPT"); + + /** The product associated with the call is unknown. */ + public static final CallReason UNKNOWN_PRODUCT = new CallReason("UNKNOWN_PRODUCT"); + + /** There are no more routes to complete the call. */ + public static final CallReason NO_MORE_ROUTES = new CallReason("NO_MORE_ROUTES"); + + /** An error occurred during the call. */ + public static final CallReason ERROR = new CallReason("ERROR"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + CallReason.class, + CallReason::new, + Arrays.asList( + OK, + NOT_AVAILABLE, + CALLER_HANGUP, + CALLEE_HANGUP, + MANAGER_HANGUP, + DID_NOT_FOUND, + INVALID_SCRIPT, + UNKNOWN_PRODUCT, + NO_MORE_ROUTES, + ERROR)); + + private CallReason(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CallReason from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CallReason e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallResult.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallResult.java new file mode 100644 index 000000000..9cc2e505f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallResult.java @@ -0,0 +1,73 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * The outcome/state of the call. + * + *

This field includes both transitional states (during call setup and execution) and + * final states (when the call has ended). + */ +public class CallResult extends EnumDynamic { + + /** The call is queued for initiation. Transitional state. */ + public static final CallResult QUEUED = new CallResult("QUEUED"); + + /** The call is connecting but the recipient has not yet answered. Transitional state. */ + public static final CallResult INITIATED = new CallResult("INITIATED"); + + /** The call has been answered and is in progress. Transitional state. */ + public static final CallResult IN_PROGRESS = new CallResult("IN_PROGRESS"); + + /** The call was answered and is ended. Final state. */ + public static final CallResult COMPLETED = new CallResult("COMPLETED"); + + /** The call was rejected by the recipient. */ + public static final CallResult REJECTED = new CallResult("REJECTED"); + + /** The call was not answered by the recipient. */ + public static final CallResult NO_ANSWER = new CallResult("NO_ANSWER"); + + /** The call was cancelled. */ + public static final CallResult CANCEL = new CallResult("CANCEL"); + + /** The call was not answered because the recipient was busy. */ + public static final CallResult BUSY = new CallResult("BUSY"); + + /** The call could not be completed. */ + public static final CallResult FAILED = new CallResult("FAILED"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + CallResult.class, + CallResult::new, + Arrays.asList( + QUEUED, + INITIATED, + IN_PROGRESS, + COMPLETED, + REJECTED, + NO_ANSWER, + CANCEL, + BUSY, + FAILED)); + + private CallResult(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CallResult from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CallResult e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallType.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallType.java new file mode 100644 index 000000000..c21fe576d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/CallType.java @@ -0,0 +1,47 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * The type of channel used for the call. + * + *

This value indicates what kind of endpoint the call is connected to (PSTN phone number or + * WebSocket stream). + */ +public class CallType extends EnumDynamic { + + /** A call from or to the telephone network. */ + public static final CallType PHONE = new CallType("PHONE"); + + /** A call from or to a SIP endpoint. */ + public static final CallType SIP = new CallType("SIP"); + + /** A call to a stream. */ + public static final CallType STREAM = new CallType("STREAM"); + + /** A call to voice relay service. */ + public static final CallType VOICE_RELAY = new CallType("VOICE_RELAY"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + CallType.class, CallType::new, Arrays.asList(PHONE, SIP, STREAM, VOICE_RELAY)); + + private CallType(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static CallType from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(CallType e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/Money.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/Money.java new file mode 100644 index 000000000..539f93ce8 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/Money.java @@ -0,0 +1,66 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** A monetary amount with an associated currency code. */ +@JsonDeserialize(builder = MoneyImpl.Builder.class) +public interface Money { + + /** + * The 3-letter currency code defined in ISO 4217. + * + *

Field is required + * + * @return currencyCode + */ + String getCurrencyCode(); + + /** + * The monetary amount as a string to preserve precision. Supports up to 4 decimal places (e.g., + * "10.5000", "0.9999"). + * + *

Field is required + * + * @return amount + */ + String getAmount(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new MoneyImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param currencyCode see getter + * @return Current builder + * @see #getCurrencyCode + */ + Builder setCurrencyCode(String currencyCode); + + /** + * see getter + * + * @param amount see getter + * @return Current builder + * @see #getAmount + */ + Builder setAmount(String amount); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + Money build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/MoneyImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/MoneyImpl.java new file mode 100644 index 000000000..7d3c725a5 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/MoneyImpl.java @@ -0,0 +1,115 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({MoneyImpl.JSON_PROPERTY_CURRENCY_CODE, MoneyImpl.JSON_PROPERTY_AMOUNT}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class MoneyImpl implements Money { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_CURRENCY_CODE = "currencyCode"; + + private OptionalValue currencyCode; + + public static final String JSON_PROPERTY_AMOUNT = "amount"; + + private OptionalValue amount; + + public MoneyImpl() {} + + protected MoneyImpl(OptionalValue currencyCode, OptionalValue amount) { + this.currencyCode = currencyCode; + this.amount = amount; + } + + @JsonIgnore + public String getCurrencyCode() { + return currencyCode.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CURRENCY_CODE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue currencyCode() { + return currencyCode; + } + + @JsonIgnore + public String getAmount() { + return amount.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_AMOUNT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue amount() { + return amount; + } + + /** Return true if this Money object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MoneyImpl money = (MoneyImpl) o; + return Objects.equals(this.currencyCode, money.currencyCode) + && Objects.equals(this.amount, money.amount); + } + + @Override + public int hashCode() { + return Objects.hash(currencyCode, amount); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class MoneyImpl {\n"); + sb.append(" currencyCode: ").append(toIndentedString(currencyCode)).append("\n"); + sb.append(" amount: ").append(toIndentedString(amount)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements Money.Builder { + OptionalValue currencyCode = OptionalValue.empty(); + OptionalValue amount = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_CURRENCY_CODE, required = true) + public Builder setCurrencyCode(String currencyCode) { + this.currencyCode = OptionalValue.of(currencyCode); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_AMOUNT, required = true) + public Builder setAmount(String amount) { + this.amount = OptionalValue.of(amount); + return this; + } + + public Money build() { + return new MoneyImpl(currencyCode, amount); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/OriginationType.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/OriginationType.java new file mode 100644 index 000000000..43913bfa7 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/OriginationType.java @@ -0,0 +1,43 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Indicates the origin/source of the call. + * + *

This describes how the call was initiated (PSTN or via the API). + */ +public class OriginationType extends EnumDynamic { + + /** The call originated from the telephone network (PSTN). */ + public static final OriginationType PHONE = new OriginationType("PHONE"); + + /** The call originated from a SIP trunk. */ + public static final OriginationType SIP = new OriginationType("SIP"); + + /** The call originated through the Sinch API. */ + public static final OriginationType SERVER = new OriginationType("SERVER"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + OriginationType.class, OriginationType::new, Arrays.asList(PHONE, SIP, SERVER)); + + private OriginationType(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static OriginationType from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(OriginationType e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/Session.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/Session.java new file mode 100644 index 000000000..aa70f850c --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/Session.java @@ -0,0 +1,184 @@ +package com.sinch.sdk.domains.voice.models.v2.sessions.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.SessionState; +import java.time.Instant; +import java.util.List; + +/** + * A session represents the complete lifecycle of a voice interaction initiated through the Voice + * API. + * + *

A session can contain one or more related call legs (for example, an original outbound call + * plus additional dials or transfers triggered by SVAML commands). Use {@link #getCalls()} to + * inspect all call legs associated with the session and {@link #getState()} to determine whether + * the session is still ongoing or has reached a final state. + */ +@JsonDeserialize(builder = SessionImpl.Builder.class) +public interface Session { + + /** + * The ID of the session. + * + *

Field is required + * + * @return sessionId + */ + String getSessionId(); + + /** + * The ID of the project associated with the call. + * + *

Field is required + * + * @return projectId + */ + String getProjectId(); + + /** + * The ID of the service used. + * + *

Field is required + * + * @return serviceId + */ + String getServiceId(); + + /** + * Call legs associated with the session. + * + *

Field is required + * + * @return calls + */ + List getCalls(); + + /** + * Timestamp (RFC 3339) indicating when the session was created. + * + *

Field is required + * + * @return createTime + */ + Instant getCreateTime(); + + /** + * Timestamp (RFC 3339) indicating when the session was last updated. + * + *

Omitted if no updates were performed on this session. + * + * @return updateTime + */ + Instant getUpdateTime(); + + /** + * Timestamp (RFC 3339) indicating when the session ended. + * + *

Omitted for ongoing sessions. + * + * @return endTime + */ + Instant getEndTime(); + + /** + * Current state of the session. + * + *

Field is required + * + * @return state + */ + SessionState getState(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SessionImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param sessionId see getter + * @return Current builder + * @see #getSessionId + */ + Builder setSessionId(String sessionId); + + /** + * see getter + * + * @param projectId see getter + * @return Current builder + * @see #getProjectId + */ + Builder setProjectId(String projectId); + + /** + * see getter + * + * @param serviceId see getter + * @return Current builder + * @see #getServiceId + */ + Builder setServiceId(String serviceId); + + /** + * see getter + * + * @param calls see getter + * @return Current builder + * @see #getCalls + */ + Builder setCalls(List calls); + + /** + * see getter + * + * @param createTime see getter + * @return Current builder + * @see #getCreateTime + */ + Builder setCreateTime(Instant createTime); + + /** + * see getter + * + * @param updateTime see getter + * @return Current builder + * @see #getUpdateTime + */ + Builder setUpdateTime(Instant updateTime); + + /** + * see getter + * + * @param endTime see getter + * @return Current builder + * @see #getEndTime + */ + Builder setEndTime(Instant endTime); + + /** + * see getter + * + * @param state see getter + * @return Current builder + * @see #getState + */ + Builder setState(SessionState state); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + Session build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/SessionImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/SessionImpl.java new file mode 100644 index 000000000..9bbdcf855 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sessions/response/SessionImpl.java @@ -0,0 +1,288 @@ +package com.sinch.sdk.domains.voice.models.v2.sessions.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.SessionState; +import java.time.Instant; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + SessionImpl.JSON_PROPERTY_SESSION_ID, + SessionImpl.JSON_PROPERTY_PROJECT_ID, + SessionImpl.JSON_PROPERTY_SERVICE_ID, + SessionImpl.JSON_PROPERTY_CALLS, + SessionImpl.JSON_PROPERTY_CREATE_TIME, + SessionImpl.JSON_PROPERTY_UPDATE_TIME, + SessionImpl.JSON_PROPERTY_END_TIME, + SessionImpl.JSON_PROPERTY_STATE +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SessionImpl implements Session { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_SESSION_ID = "sessionId"; + + private OptionalValue sessionId; + + public static final String JSON_PROPERTY_PROJECT_ID = "projectId"; + + private OptionalValue projectId; + + public static final String JSON_PROPERTY_SERVICE_ID = "serviceId"; + + private OptionalValue serviceId; + + public static final String JSON_PROPERTY_CALLS = "calls"; + + private OptionalValue> calls; + + public static final String JSON_PROPERTY_CREATE_TIME = "createTime"; + + private OptionalValue createTime; + + public static final String JSON_PROPERTY_UPDATE_TIME = "updateTime"; + + private OptionalValue updateTime; + + public static final String JSON_PROPERTY_END_TIME = "endTime"; + + private OptionalValue endTime; + + public static final String JSON_PROPERTY_STATE = "state"; + + private OptionalValue state; + + public SessionImpl() {} + + protected SessionImpl( + OptionalValue sessionId, + OptionalValue projectId, + OptionalValue serviceId, + OptionalValue> calls, + OptionalValue createTime, + OptionalValue updateTime, + OptionalValue endTime, + OptionalValue state) { + this.sessionId = sessionId; + this.projectId = projectId; + this.serviceId = serviceId; + this.calls = calls; + this.createTime = createTime; + this.updateTime = updateTime; + this.endTime = endTime; + this.state = state; + } + + @JsonIgnore + public String getSessionId() { + return sessionId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SESSION_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue sessionId() { + return sessionId; + } + + @JsonIgnore + public String getProjectId() { + return projectId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PROJECT_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue projectId() { + return projectId; + } + + @JsonIgnore + public String getServiceId() { + return serviceId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SERVICE_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue serviceId() { + return serviceId; + } + + @JsonIgnore + public List getCalls() { + return calls.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALLS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> calls() { + return calls; + } + + @JsonIgnore + public Instant getCreateTime() { + return createTime.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CREATE_TIME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue createTime() { + return createTime; + } + + @JsonIgnore + public Instant getUpdateTime() { + return updateTime.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_UPDATE_TIME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue updateTime() { + return updateTime; + } + + @JsonIgnore + public Instant getEndTime() { + return endTime.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_END_TIME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue endTime() { + return endTime; + } + + @JsonIgnore + public SessionState getState() { + return state.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_STATE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue state() { + return state; + } + + /** Return true if this Session object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SessionImpl session = (SessionImpl) o; + return Objects.equals(this.sessionId, session.sessionId) + && Objects.equals(this.projectId, session.projectId) + && Objects.equals(this.serviceId, session.serviceId) + && Objects.equals(this.calls, session.calls) + && Objects.equals(this.createTime, session.createTime) + && Objects.equals(this.updateTime, session.updateTime) + && Objects.equals(this.endTime, session.endTime) + && Objects.equals(this.state, session.state); + } + + @Override + public int hashCode() { + return Objects.hash( + sessionId, projectId, serviceId, calls, createTime, updateTime, endTime, state); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SessionImpl {\n"); + sb.append(" sessionId: ").append(toIndentedString(sessionId)).append("\n"); + sb.append(" projectId: ").append(toIndentedString(projectId)).append("\n"); + sb.append(" serviceId: ").append(toIndentedString(serviceId)).append("\n"); + sb.append(" calls: ").append(toIndentedString(calls)).append("\n"); + sb.append(" createTime: ").append(toIndentedString(createTime)).append("\n"); + sb.append(" updateTime: ").append(toIndentedString(updateTime)).append("\n"); + sb.append(" endTime: ").append(toIndentedString(endTime)).append("\n"); + sb.append(" state: ").append(toIndentedString(state)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements Session.Builder { + OptionalValue sessionId = OptionalValue.empty(); + OptionalValue projectId = OptionalValue.empty(); + OptionalValue serviceId = OptionalValue.empty(); + OptionalValue> calls = OptionalValue.empty(); + OptionalValue createTime = OptionalValue.empty(); + OptionalValue updateTime = OptionalValue.empty(); + OptionalValue endTime = OptionalValue.empty(); + OptionalValue state = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_SESSION_ID, required = true) + public Builder setSessionId(String sessionId) { + this.sessionId = OptionalValue.of(sessionId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_PROJECT_ID, required = true) + public Builder setProjectId(String projectId) { + this.projectId = OptionalValue.of(projectId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SERVICE_ID, required = true) + public Builder setServiceId(String serviceId) { + this.serviceId = OptionalValue.of(serviceId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CALLS, required = true) + public Builder setCalls(List calls) { + this.calls = OptionalValue.of(calls); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CREATE_TIME, required = true) + public Builder setCreateTime(Instant createTime) { + this.createTime = OptionalValue.of(createTime); + return this; + } + + @JsonProperty(JSON_PROPERTY_UPDATE_TIME) + public Builder setUpdateTime(Instant updateTime) { + this.updateTime = OptionalValue.of(updateTime); + return this; + } + + @JsonProperty(JSON_PROPERTY_END_TIME) + public Builder setEndTime(Instant endTime) { + this.endTime = OptionalValue.of(endTime); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_STATE, required = true) + public Builder setState(SessionState state) { + this.state = OptionalValue.of(state); + return this; + } + + public Session build() { + return new SessionImpl( + sessionId, projectId, serviceId, calls, createTime, updateTime, endTime, state); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/CallDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/CallDtoTest.java new file mode 100644 index 000000000..e84c5ba04 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/CallDtoTest.java @@ -0,0 +1,77 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.destination.PhoneDtoTest; +import com.sinch.sdk.domains.voice.models.v2.destination.SipFromDtoTest; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class CallDtoTest extends BaseTest { + + public static final Call expectedCall = + Call.builder() + .setCallId("01ARZ3NDEKTSV4RRFFQ69G5FAA") + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setSessionId("01BX5ZZKBKACTAV9WEVGEMMVRB") + .setCallName("origin") + .setBridgeName("my-bridge") + .setBatchId("01BX5ZZKBKACTAV9WEVGEMMVRC") + .setFrom(SipFromDtoTest.expectedSipFrom) + .setTo(PhoneDtoTest.expectedPhone) + .setStartTime(Instant.parse("2025-02-10T09:00:00Z")) + .setUpdateTime(Instant.parse("2025-02-10T09:00:47Z")) + .setCallType(CallType.PHONE) + .setDirection(CallDirection.INBOUND) + .setAnswerTime(Instant.parse("2025-02-10T09:00:05Z")) + .setEndTime(Instant.parse("2025-02-10T09:00:47Z")) + .setCallDurationSeconds(42) + .setCallResult(CallResult.COMPLETED) + .setCallReason(CallReason.CALLEE_HANGUP) + .setOriginationType(OriginationType.SIP) + .setCallRate(MoneyDtoTest.expectedMoney) + .setCallResourceUrl( + "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA") + .build(); + + public static final Call expectedCallMinimal = + Call.builder() + .setCallId("01ARZ3NDEKTSV4RRFFQ69G5FAA") + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setSessionId("01BX5ZZKBKACTAV9WEVGEMMVRB") + .setStartTime(Instant.parse("2025-02-10T09:00:00Z")) + .setCallType(CallType.PHONE) + .setDirection(CallDirection.OUTBOUND) + .setCallResult(CallResult.QUEUED) + .setOriginationType(OriginationType.SERVER) + .setCallRate(MoneyDtoTest.expectedMoney) + .setCallResourceUrl( + "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA") + .build(); + + @GivenTextResource("/domains/voice/v2/CallDto.json") + String jsonCall; + + @GivenTextResource("/domains/voice/v2/CallMinimalDto.json") + String jsonCallMinimal; + + @Test + void deserialize() throws JsonProcessingException { + Call deserialized = objectMapper.readValue(jsonCall, Call.class); + + TestHelpers.recursiveEquals(deserialized, expectedCall); + } + + @Test + void deserializeMinimalDto() throws JsonProcessingException { + Call deserialized = objectMapper.readValue(jsonCallMinimal, Call.class); + + TestHelpers.recursiveEquals(deserialized, expectedCallMinimal); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/MoneyDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/MoneyDtoTest.java new file mode 100644 index 000000000..64f91460b --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/MoneyDtoTest.java @@ -0,0 +1,25 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class MoneyDtoTest extends BaseTest { + + public static final Money expectedMoney = + Money.builder().setCurrencyCode("USD").setAmount("0.0123").build(); + + @GivenTextResource("/domains/voice/v2/MoneyDto.json") + String jsonMoney; + + @Test + void deserialize() throws JsonProcessingException { + Money deserialized = objectMapper.readValue(jsonMoney, Money.class); + + TestHelpers.recursiveEquals(deserialized, expectedMoney); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sessions/response/SessionDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sessions/response/SessionDtoTest.java new file mode 100644 index 000000000..b0097fd9c --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sessions/response/SessionDtoTest.java @@ -0,0 +1,58 @@ +package com.sinch.sdk.domains.voice.models.v2.sessions.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.CallDtoTest; +import com.sinch.sdk.domains.voice.models.v2.SessionState; +import java.time.Instant; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class SessionDtoTest extends BaseTest { + + public static final Session expectedSession = + Session.builder() + .setSessionId("01BX5ZZKBKACTAV9WEVGEMMVRB") + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setCalls(Collections.singletonList(CallDtoTest.expectedCall)) + .setCreateTime(Instant.parse("2025-02-10T09:00:00Z")) + .setUpdateTime(Instant.parse("2025-02-10T09:00:47Z")) + .setEndTime(Instant.parse("2025-02-10T09:00:47Z")) + .setState(SessionState.COMPLETED) + .build(); + + public static final Session expectedSessionMinimal = + Session.builder() + .setSessionId("01BX5ZZKBKACTAV9WEVGEMMVRB") + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setCalls(Collections.singletonList(CallDtoTest.expectedCallMinimal)) + .setCreateTime(Instant.parse("2025-02-10T09:00:00Z")) + .setState(SessionState.QUEUED) + .build(); + + @GivenTextResource("/domains/voice/v2/sessions/response/SessionDto.json") + String jsonSession; + + @GivenTextResource("/domains/voice/v2/sessions/response/SessionMinimalDto.json") + String jsonSessionMinimal; + + @Test + void deserialize() throws JsonProcessingException { + Session deserialized = objectMapper.readValue(jsonSession, Session.class); + + TestHelpers.recursiveEquals(deserialized, expectedSession); + } + + @Test + void deserializeMinimalDto() throws JsonProcessingException { + Session deserialized = objectMapper.readValue(jsonSessionMinimal, Session.class); + + TestHelpers.recursiveEquals(deserialized, expectedSessionMinimal); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/CallDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/CallDto.json new file mode 100644 index 000000000..0114f2001 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/CallDto.json @@ -0,0 +1,37 @@ +{ + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "callName": "origin", + "bridgeName": "my-bridge", + "batchId": "01BX5ZZKBKACTAV9WEVGEMMVRC", + "from": { + "type": "SIP", + "sip": { + "endpoint": "sip:alice@sip.example.com", + "displayName": "Alice" + } + }, + "to": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "startTime": "2025-02-10T09:00:00Z", + "updateTime": "2025-02-10T09:00:47Z", + "callType": "PHONE", + "direction": "INBOUND", + "answerTime": "2025-02-10T09:00:05Z", + "endTime": "2025-02-10T09:00:47Z", + "callDurationSeconds": 42, + "callResult": "COMPLETED", + "callReason": "CALLEE_HANGUP", + "originationType": "SIP", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/CallMinimalDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/CallMinimalDto.json new file mode 100644 index 000000000..1f5843e3d --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/CallMinimalDto.json @@ -0,0 +1,16 @@ +{ + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "startTime": "2025-02-10T09:00:00Z", + "callType": "PHONE", + "direction": "OUTBOUND", + "callResult": "QUEUED", + "originationType": "SERVER", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/MoneyDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/MoneyDto.json new file mode 100644 index 000000000..7b08cf3fd --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/MoneyDto.json @@ -0,0 +1,4 @@ +{ + "currencyCode": "USD", + "amount": "0.0123" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/sessions/response/SessionDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/sessions/response/SessionDto.json new file mode 100644 index 000000000..cea02a135 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/sessions/response/SessionDto.json @@ -0,0 +1,48 @@ +{ + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "calls": [ + { + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "callName": "origin", + "bridgeName": "my-bridge", + "batchId": "01BX5ZZKBKACTAV9WEVGEMMVRC", + "from": { + "type": "SIP", + "sip": { + "endpoint": "sip:alice@sip.example.com", + "displayName": "Alice" + } + }, + "to": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "startTime": "2025-02-10T09:00:00Z", + "updateTime": "2025-02-10T09:00:47Z", + "callType": "PHONE", + "direction": "INBOUND", + "answerTime": "2025-02-10T09:00:05Z", + "endTime": "2025-02-10T09:00:47Z", + "callDurationSeconds": 42, + "callResult": "COMPLETED", + "callReason": "CALLEE_HANGUP", + "originationType": "SIP", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" + } + ], + "createTime": "2025-02-10T09:00:00Z", + "updateTime": "2025-02-10T09:00:47Z", + "endTime": "2025-02-10T09:00:47Z", + "state": "COMPLETED" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/sessions/response/SessionMinimalDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/sessions/response/SessionMinimalDto.json new file mode 100644 index 000000000..bee95389e --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/sessions/response/SessionMinimalDto.json @@ -0,0 +1,25 @@ +{ + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "calls": [ + { + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "startTime": "2025-02-10T09:00:00Z", + "callType": "PHONE", + "direction": "OUTBOUND", + "callResult": "QUEUED", + "originationType": "SERVER", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" + } + ], + "createTime": "2025-02-10T09:00:00Z", + "state": "QUEUED" +} From 03b7f332b52f5c310a9c4f4e7471757b1addc8a4 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Tue, 6 Oct 2026 15:10:34 +0200 Subject: [PATCH 24/50] Aligned of a variable in the BatchesServiceTest --- .../api/v2/adapters/BatchesServiceTest.java | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java index 7a378a7f5..2fdaeca45 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java @@ -48,6 +48,7 @@ public class BatchesServiceTest extends BaseTest { @Mock Map authManagers; static final String PROJECT_ID = "test_project_id"; + static final String BATCH_ID = "01BX5ZZKBKACTAV9WEVGEMMVRC"; static final Collection AUTH_NAMES = Arrays.asList("BasicAuth", "SinchOAuth2"); static final Collection ACCEPTS = Arrays.asList(HttpContentType.APPLICATION_JSON, "application/problem+json"); @@ -139,13 +140,12 @@ void startMissingProjectIdThrows() { @Test void get() throws ApiException { - String batchId = BatchSummaryDtoTest.expectedBatchSummary.getBatchId(); HttpRequest httpRequest = new HttpRequest( "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/batches/" - + URLPathUtils.encodePathSegment(batchId), + + URLPathUtils.encodePathSegment(BATCH_ID), HttpMethod.GET, Collections.emptyList(), (String) null, @@ -162,7 +162,7 @@ void get() throws ApiException { argThat(new HttpRequestMatcher(httpRequest)))) .thenReturn(httpResponse); - BatchSummary response = service.get(batchId); + BatchSummary response = service.get(BATCH_ID); TestHelpers.recursiveEquals(response, BatchSummaryDtoTest.expectedBatchSummary); } @@ -183,8 +183,7 @@ void getMissingProjectIdThrows() { httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); ApiException thrown = - Assertions.assertThrows( - ApiException.class, () -> serviceWithoutProjectId.get("01BX5ZZKBKACTAV9WEVGEMMVRC")); + Assertions.assertThrows(ApiException.class, () -> serviceWithoutProjectId.get(BATCH_ID)); Assertions.assertEquals(400, thrown.getCode()); } @@ -192,13 +191,12 @@ void getMissingProjectIdThrows() { @Test void getDetails() throws ApiException { - String batchId = "01BX5ZZKBKACTAV9WEVGEMMVRC"; HttpRequest httpRequest = new HttpRequest( "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/batches/" - + URLPathUtils.encodePathSegment(batchId) + + URLPathUtils.encodePathSegment(BATCH_ID) + "/details", HttpMethod.GET, Collections.emptyList(), @@ -216,7 +214,7 @@ void getDetails() throws ApiException { argThat(new HttpRequestMatcher(httpRequest)))) .thenReturn(httpResponse); - BatchDetails response = service.getDetails(batchId); + BatchDetails response = service.getDetails(BATCH_ID); TestHelpers.recursiveEquals(response, BatchDetailsDtoTest.expectedBatchDetails); } @@ -239,8 +237,7 @@ void getDetailsMissingProjectIdThrows() { ApiException thrown = Assertions.assertThrows( - ApiException.class, - () -> serviceWithoutProjectId.getDetails("01BX5ZZKBKACTAV9WEVGEMMVRC")); + ApiException.class, () -> serviceWithoutProjectId.getDetails(BATCH_ID)); Assertions.assertEquals(400, thrown.getCode()); } @@ -248,13 +245,12 @@ void getDetailsMissingProjectIdThrows() { @Test void stop() throws ApiException { - String batchId = "01BX5ZZKBKACTAV9WEVGEMMVRC"; HttpRequest httpRequest = new HttpRequest( "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/batches/" - + URLPathUtils.encodePathSegment(batchId), + + URLPathUtils.encodePathSegment(BATCH_ID), HttpMethod.DELETE, Collections.emptyList(), (String) null, @@ -271,7 +267,7 @@ void stop() throws ApiException { argThat(new HttpRequestMatcher(httpRequest)))) .thenReturn(httpResponse); - BatchStopResponse response = service.stop(batchId); + BatchStopResponse response = service.stop(BATCH_ID); TestHelpers.recursiveEquals(response, BatchStopResponseDtoTest.expectedBatchStopResponse); } @@ -292,8 +288,7 @@ void stopMissingProjectIdThrows() { httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); ApiException thrown = - Assertions.assertThrows( - ApiException.class, () -> serviceWithoutProjectId.stop("01BX5ZZKBKACTAV9WEVGEMMVRC")); + Assertions.assertThrows(ApiException.class, () -> serviceWithoutProjectId.stop(BATCH_ID)); Assertions.assertEquals(400, thrown.getCode()); } From ed7ec560f1e3d649d4b1fa9bd08a1b5504b8cb80 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Wed, 7 Oct 2026 17:08:07 +0200 Subject: [PATCH 25/50] DEVEXP-1624: Add Voice v2 Sinch Events --- CHANGELOG.md | 2 + .../voice/api/v2/SinchEventsService.java | 91 +++++++ .../domains/voice/api/v2/VoiceService.java | 10 + .../api/v2/adapters/SinchEventsService.java | 126 +++++++++ .../voice/api/v2/adapters/VoiceService.java | 13 + .../models/v2/sinchevents/package-info.java | 6 + .../v2/adapters/SinchEventsServiceTest.java | 253 ++++++++++++++++++ .../api/v2/adapters/VoiceServiceTest.java | 9 + .../sinch/sdk/e2e/domains/WebhooksHelper.java | 15 ++ .../domains/voice/v2/SinchEventsSteps.java | 166 ++++++++++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 4 +- examples/sinch-events/README.md | 5 +- .../mycompany/app/voice/v2/Controller.java | 104 +++++++ .../app/voice/v2/ServerBusinessLogic.java | 75 ++++++ .../src/main/resources/application.yaml | 8 + .../models/v2/sinchevents/MenuInput.java | 70 +++++ .../models/v2/sinchevents/MenuInputImpl.java | 115 ++++++++ .../models/v2/sinchevents/SinchEventType.java | 110 ++++++++ .../v2/sinchevents/VoiceSinchEvent.java | 87 ++++++ .../v2/sinchevents/VoiceSinchEventImpl.java | 148 ++++++++++ .../voice/models/v2/svaml/SvamlInput.java | 99 +++++++ .../models/v2/svaml/SvamlInputEvents.java | 45 ++++ .../models/v2/svaml/SvamlInputEventsImpl.java | 91 +++++++ .../voice/models/v2/svaml/SvamlInputImpl.java | 148 ++++++++++ .../v2/sinchevents/MenuInputDtoTest.java | 25 ++ .../sinchevents/VoiceSinchEventDtoTest.java | 51 ++++ .../models/v2/svaml/SvamlInputDtoTest.java | 32 +++ .../v2/svaml/SvamlInputEventsDtoTest.java | 30 +++ .../voice/v2/sinchevents/MenuInputDto.json | 4 + .../VoiceSinchEventCustomEventDto.json | 40 +++ .../v2/sinchevents/VoiceSinchEventDto.json | 44 +++ .../domains/voice/v2/svaml/SvamlInputDto.json | 17 ++ .../voice/v2/svaml/SvamlInputEventsDto.json | 8 + 33 files changed, 2049 insertions(+), 2 deletions(-) create mode 100644 client/src/main/com/sinch/sdk/domains/voice/api/v2/SinchEventsService.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsService.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/package-info.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsServiceTest.java create mode 100644 client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SinchEventsSteps.java create mode 100644 examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/Controller.java create mode 100644 examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInput.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInputImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/SinchEventType.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEvent.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEvents.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInputDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/MenuInputDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventCustomEventDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputEventsDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d09e6a480..e69b6ba9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,12 +25,14 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Batches API: `getDetails` operation to retrieve the per-session details of a batch - **[feature]** Voice V2 Batches API: `stop` operation to stop processing a batch of calls - **[feature]** Voice V2 Sessions API: `get` operation to retrieve a session and its calls +- **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/batches/Start` snippet - **[doc]** New `voice/v2/batches/Get` snippet - **[doc]** New `voice/v2/batches/GetDetails` snippet - **[doc]** New `voice/v2/batches/Stop` snippet - **[doc]** New `voice/v2/sessions/Get` snippet +- **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` ## v2.2.0 - 2026-09-17 diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/SinchEventsService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/SinchEventsService.java new file mode 100644 index 000000000..89a3d57f9 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/SinchEventsService.java @@ -0,0 +1,91 @@ +package com.sinch.sdk.domains.voice.api.v2; + +import com.sinch.sdk.core.exceptions.ApiMappingException; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import java.util.Map; + +/** + * Sinch Events service + * + *

Helpers to handle, from your backend, the Sinch Events sent by the Voice Platform: validate + * their signature, parse their payload, and serialize the SVAML document to respond with. + * + *

This service requires no credentials from the client configuration: the service credentials + * used to validate a Sinch Event are given on each call. + * + * @see https://developers.sinch.com/docs/voice-2.0 + * @since 2.3 + */ +public interface SinchEventsService { + + /** + * The Voice Platform signs every Sinch Event request with the secret of the service the event + * belongs to. The signature is carried in the Authorization header, using the + * service scheme: Authorization: service <serviceId>:<signature> + * + *

By using following function, you can ensure authentication according to received payload + * from your backend. The request is rejected when: + * + *

    + *
  • the Authorization or the x-timestamp header is missing + *
  • the scheme is not service + *
  • the service ID in the header differs from serviceId + *
  • the signature does not match the one computed from the received request + *
+ * + *

Replay protection is the caller's responsibility: this function does not check the + * x-timestamp header against the current time. Reject requests whose + * x-timestamp lies outside the clock-skew window your application accepts. + * + * @param serviceId The ID of the service the Sinch Event belongs to + * @param serviceSecret The secret of the service, as issued (Base64 encoded) + * @param method The HTTP method used to handle the Sinch Event (always POST for + * Voice Platform requests) + * @param path The path to your backend endpoint used for the Sinch Event (scheme, host, query + * string and fragment are not part of the signature) + * @param headers Received headers + * @param jsonPayload Received payload, exactly as delivered (before any parsing or + * re-serialization) + * @return Is authentication validated (true) or not (false) + * @throws IllegalArgumentException if serviceId or serviceSecret is + * empty, or if serviceSecret is not valid Base64 + * @since 2.3 + */ + boolean validateAuthenticationHeader( + String serviceId, + String serviceSecret, + String method, + String path, + Map headers, + String jsonPayload); + + /** + * This function can be called to deserialize received payload onto Voice Sinch Event class + * + *

CloudEvents headers (ce-*) are not part of the returned event. + * + *

Custom events, triggered by the {@link + * com.sinch.sdk.domains.voice.models.v2.svaml.customevents.CustomEventCommand}, are sent by the + * API as call.webhook.<name> and returned as + * call.customEvent.<name>. + * + * @param jsonPayload Received payload to be deserialized + * @return The Voice Sinch Event + * @throws ApiMappingException if the payload cannot be deserialized + * @since 2.3 + */ + VoiceSinchEvent parseEvent(String jsonPayload) throws ApiMappingException; + + /** + * This function can be called to serialize the SVAML document to be sent in response to a Sinch + * Event + * + * @param response The response to be serialized + * @return The JSON string to be sent + * @throws ApiMappingException if the response cannot be serialized + * @since 2.3 + */ + String serializeResponse(SvamlInput response) throws ApiMappingException; +} diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java index cdc3f87b6..8a331b553 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java @@ -32,4 +32,14 @@ public interface VoiceService { * @since 2.3 */ SessionsService sessions(); + + /** + * Sinch Events Service instance + * + *

Requires no credentials from the client configuration. + * + * @return service instance + * @since 2.3 + */ + SinchEventsService sinchEvents(); } diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsService.java new file mode 100644 index 000000000..c6847fcf2 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsService.java @@ -0,0 +1,126 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.sinch.sdk.auth.adapters.ApplicationAuthManager; +import com.sinch.sdk.core.exceptions.ApiMappingException; +import com.sinch.sdk.core.utils.MapUtils; +import com.sinch.sdk.core.utils.Pair; +import com.sinch.sdk.core.utils.StringUtil; +import com.sinch.sdk.core.utils.databind.Mapper; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Locale; +import java.util.Map; + +public class SinchEventsService implements com.sinch.sdk.domains.voice.api.v2.SinchEventsService { + + private static final String AUTH_KEYWORD = "service"; + private static final String XTIMESTAMP_HEADER = "x-timestamp"; + + // The API sends custom events as "call.webhook.": the SDK names them after the + // customEvent SVAML command, like the Python SDK does + private static final String WIRE_CUSTOM_EVENT_PREFIX = "call.webhook."; + private static final String CUSTOM_EVENT_PREFIX = "call.customEvent."; + + public boolean validateAuthenticationHeader( + String serviceId, + String serviceSecret, + String method, + String path, + Map headers, + String jsonPayload) { + + StringUtil.requireNonEmpty(serviceId, "'serviceId' must be defined"); + StringUtil.requireNonEmpty(serviceSecret, "'serviceSecret' must be defined"); + + // throws IllegalArgumentException on an invalid Base64 secret + ApplicationAuthManager signer = new ApplicationAuthManager(serviceId, serviceSecret); + + if (null == headers) { + return false; + } + + // convert header keys to use case-insensitive map keys + Map caseInsensitiveHeaders = MapUtils.getCaseInsensitiveMap(headers); + + String authorizationHeader = caseInsensitiveHeaders.get("Authorization"); + if (StringUtil.isEmpty(authorizationHeader)) { + return false; + } + + String[] split = authorizationHeader.split(" ", 2); + if (split.length != 2 || !AUTH_KEYWORD.equalsIgnoreCase(split[0])) { + return false; + } + + // signature is Base64 and contains no colon: split on the last one + String authorizationValue = split[1]; + int separator = authorizationValue.lastIndexOf(':'); + if (separator < 0) { + return false; + } + if (!serviceId.equals(authorizationValue.substring(0, separator))) { + return false; + } + String receivedSignature = authorizationValue.substring(separator + 1); + + String timestamp = caseInsensitiveHeaders.get(XTIMESTAMP_HEADER); + if (StringUtil.isEmpty(timestamp)) { + return false; + } + String contentType = caseInsensitiveHeaders.getOrDefault("content-type", ""); + + // Voice V2 "service" scheme shares the signing algorithm of the "Application" scheme + String computedSignature = + signer + .getAuthorizationHeaders( + timestamp, + null == method ? null : method.toUpperCase(Locale.ROOT), + contentType, + path, + jsonPayload) + .stream() + .filter(f -> f.getLeft().equals("Authorization")) + .findFirst() + .map(Pair::getRight) + .map(value -> value.substring(value.lastIndexOf(':') + 1)) + .orElse(""); + + return MessageDigest.isEqual( + computedSignature.getBytes(StandardCharsets.UTF_8), + receivedSignature.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public VoiceSinchEvent parseEvent(String jsonPayload) throws ApiMappingException { + try { + JsonNode node = Mapper.getInstance().readTree(jsonPayload); + JsonNode event = null != node ? node.get("event") : null; + if (node instanceof ObjectNode + && null != event + && event.isTextual() + && event.asText().startsWith(WIRE_CUSTOM_EVENT_PREFIX)) { + ((ObjectNode) node) + .put( + "event", + CUSTOM_EVENT_PREFIX + event.asText().substring(WIRE_CUSTOM_EVENT_PREFIX.length())); + } + return Mapper.getInstance().treeToValue(node, VoiceSinchEvent.class); + } catch (JsonProcessingException | IllegalArgumentException e) { + throw new ApiMappingException(jsonPayload, e); + } + } + + @Override + public String serializeResponse(SvamlInput response) throws ApiMappingException { + try { + return Mapper.getInstance().writeValueAsString(response); + } catch (JsonProcessingException e) { + throw new ApiMappingException(String.valueOf(response), e); + } + } +} diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java index ed3481b59..48e55d6eb 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java @@ -35,6 +35,7 @@ public class VoiceService implements com.sinch.sdk.domains.voice.api.v2.VoiceSer private volatile CallsService calls; private volatile BatchesService batches; private volatile SessionsService sessions; + private volatile SinchEventsService sinchEvents; public VoiceService( UnifiedCredentials credentials, @@ -101,6 +102,18 @@ public SessionsService sessions() { return this.sessions; } + public SinchEventsService sinchEvents() { + // no lazy init: validating a Sinch Event takes service credentials on each call + if (null == this.sinchEvents) { + synchronized (this) { + if (null == this.sinchEvents) { + this.sinchEvents = new SinchEventsService(); + } + } + } + return this.sinchEvents; + } + private void instanceLazyInit() { if (null != this.authManagers) { return; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/package-info.java new file mode 100644 index 000000000..3bc2d4f2e --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 Sinch Events + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsServiceTest.java new file mode 100644 index 000000000..127a27c13 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsServiceTest.java @@ -0,0 +1,253 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.core.exceptions.ApiMappingException; +import com.sinch.sdk.domains.voice.api.v2.SinchEventsService; +import com.sinch.sdk.domains.voice.models.v2.CallDtoTest; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.SinchEventType; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventDtoTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInputDtoTest; +import java.util.HashMap; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +class SinchEventsServiceTest extends BaseTest { + + // Signature example from the Voice V2 OAS "callWebhook" description + static final String SERVICE_ID = "a74b1566-0f18-4f8e-9c23-8e6b5df8fd3e"; + static final String SERVICE_SECRET = "F5wrP9SKYU6w8sbZXkp7GA=="; + static final String PATH = "/voice-webhooks"; + static final String PAYLOAD = + "{\"event\":\"call.incoming\",\"call\":{\"callId\":\"01AN4Z07BY79KA1307SR9X4MV3\"}}"; + static final String SIGNATURE = "EWFtVTrykdhMTdyYSbn40GBJpf5UBeggO9T99sdwLyY="; + + @GivenTextResource("/domains/voice/v2/sinchevents/VoiceSinchEventDto.json") + String jsonVoiceSinchEvent; + + @GivenTextResource("/domains/voice/v2/sinchevents/VoiceSinchEventCustomEventDto.json") + String jsonVoiceSinchEventCustomEvent; + + @GivenTextResource("/domains/voice/v2/svaml/SvamlInputDto.json") + String jsonSvamlInput; + + SinchEventsService service; + + @BeforeEach + public void initMocks() { + // no credentials at all: Sinch Events do not depend on the client configuration + service = new VoiceService(null, null, null, null).sinchEvents(); + } + + Map signedHeaders() { + Map headers = new HashMap<>(); + headers.put("Authorization", "service " + SERVICE_ID + ":" + SIGNATURE); + headers.put("Content-Type", "application/json; charset=utf-8"); + headers.put("x-timestamp", "2026-04-01T12:00:00.0000000Z"); + headers.put("ce-type", "com.sinch.voice.call.control.v1"); + return headers; + } + + boolean validate(Map headers, String payload) { + return service.validateAuthenticationHeader( + SERVICE_ID, SERVICE_SECRET, "POST", PATH, headers, payload); + } + + @Test + void validateAuthenticationHeader() { + Assertions.assertThat(validate(signedHeaders(), PAYLOAD)).isTrue(); + } + + @Test + void validateAuthenticationHeaderCaseInsensitive() { + Map headers = new HashMap<>(); + headers.put("authorization", "Service " + SERVICE_ID + ":" + SIGNATURE); + headers.put("content-type", "application/json; charset=utf-8"); + headers.put("X-Timestamp", "2026-04-01T12:00:00.0000000Z"); + + Assertions.assertThat(validate(headers, PAYLOAD)).isTrue(); + } + + @Test + void validateAuthenticationHeaderIgnoresQueryString() { + Assertions.assertThat( + service.validateAuthenticationHeader( + SERVICE_ID, SERVICE_SECRET, "POST", PATH + "?foo=bar", signedHeaders(), PAYLOAD)) + .isTrue(); + } + + @Test + void rejectOtherServiceIdInHeader() { + // the signature is valid for SERVICE_ID, but the header claims another service + Map headers = signedHeaders(); + headers.put("Authorization", "service other-service:" + SIGNATURE); + + Assertions.assertThat(validate(headers, PAYLOAD)).isFalse(); + } + + @Test + void rejectOtherServiceIdParameter() { + Assertions.assertThat( + service.validateAuthenticationHeader( + "other-service", SERVICE_SECRET, "POST", PATH, signedHeaders(), PAYLOAD)) + .isFalse(); + } + + @Test + void rejectOtherSecret() { + Assertions.assertThat( + service.validateAuthenticationHeader( + SERVICE_ID, "YXBwU2VjcmV0", "POST", PATH, signedHeaders(), PAYLOAD)) + .isFalse(); + } + + @Test + void rejectTamperedPayload() { + Assertions.assertThat(validate(signedHeaders(), PAYLOAD.replace("incoming", "answered"))) + .isFalse(); + } + + @Test + void rejectTamperedTimestamp() { + Map headers = signedHeaders(); + headers.put("x-timestamp", "2026-04-01T12:00:01.0000000Z"); + + Assertions.assertThat(validate(headers, PAYLOAD)).isFalse(); + } + + @Test + void rejectOtherPath() { + Assertions.assertThat( + service.validateAuthenticationHeader( + SERVICE_ID, SERVICE_SECRET, "POST", "/other", signedHeaders(), PAYLOAD)) + .isFalse(); + } + + @Test + void validateAuthenticationHeaderLowerCaseMethod() { + Assertions.assertThat( + service.validateAuthenticationHeader( + SERVICE_ID, SERVICE_SECRET, "post", PATH, signedHeaders(), PAYLOAD)) + .isTrue(); + } + + @Test + void rejectOtherMethod() { + Assertions.assertThat( + service.validateAuthenticationHeader( + SERVICE_ID, SERVICE_SECRET, "PUT", PATH, signedHeaders(), PAYLOAD)) + .isFalse(); + } + + @Test + void rejectApplicationScheme() { + Map headers = signedHeaders(); + headers.put("Authorization", "Application " + SERVICE_ID + ":" + SIGNATURE); + + Assertions.assertThat(validate(headers, PAYLOAD)).isFalse(); + } + + @Test + void rejectMalformedAuthorization() { + Map headers = signedHeaders(); + headers.put("Authorization", "service " + SIGNATURE); + + Assertions.assertThat(validate(headers, PAYLOAD)).isFalse(); + + headers.put("Authorization", "service"); + Assertions.assertThat(validate(headers, PAYLOAD)).isFalse(); + } + + @Test + void rejectEmptyAuthorization() { + Map headers = signedHeaders(); + headers.put("Authorization", ""); + + Assertions.assertThat(validate(headers, PAYLOAD)).isFalse(); + } + + @Test + void rejectMissingAuthorization() { + Map headers = signedHeaders(); + headers.remove("Authorization"); + + Assertions.assertThat(validate(headers, PAYLOAD)).isFalse(); + } + + @Test + void rejectMissingTimestamp() { + Map headers = signedHeaders(); + headers.remove("x-timestamp"); + + Assertions.assertThat(validate(headers, PAYLOAD)).isFalse(); + } + + @Test + void rejectNullHeaders() { + Assertions.assertThat(validate(null, PAYLOAD)).isFalse(); + } + + @Test + void requireServiceCredentials() { + Assertions.assertThatThrownBy( + () -> + service.validateAuthenticationHeader( + "", SERVICE_SECRET, "POST", PATH, signedHeaders(), PAYLOAD)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("serviceId"); + + Assertions.assertThatThrownBy( + () -> + service.validateAuthenticationHeader( + SERVICE_ID, null, "POST", PATH, signedHeaders(), PAYLOAD)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("serviceSecret"); + + Assertions.assertThatThrownBy( + () -> + service.validateAuthenticationHeader( + SERVICE_ID, "not base64 !", "POST", PATH, signedHeaders(), PAYLOAD)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void parseEvent() throws ApiMappingException { + VoiceSinchEvent event = service.parseEvent(jsonVoiceSinchEvent); + + TestHelpers.recursiveEquals(event, VoiceSinchEventDtoTest.expectedVoiceSinchEvent); + } + + @Test + void parseCustomEvent() throws ApiMappingException { + VoiceSinchEvent event = service.parseEvent(jsonVoiceSinchEventCustomEvent); + + // sent as "call.webhook.on-answer", returned with the customEvent naming + TestHelpers.recursiveEquals( + event, + VoiceSinchEvent.builder() + .setEvent(SinchEventType.from("call.customEvent.on-answer")) + .setCall(CallDtoTest.expectedCall) + .build()); + } + + @Test + void parseEventInvalidPayload() { + Assertions.assertThatThrownBy(() -> service.parseEvent("{not json")) + .isInstanceOf(ApiMappingException.class); + } + + @Test + void serializeResponse() throws ApiMappingException, JSONException { + String serialized = service.serializeResponse(SvamlInputDtoTest.expectedSvamlInput); + + JSONAssert.assertEquals(jsonSvamlInput, serialized, true); + } +} diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java index b165ac667..db7e19fbb 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java @@ -1,6 +1,9 @@ package com.sinch.sdk.domains.voice.api.v2.adapters; +import com.sinch.sdk.SinchClient; import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.models.Configuration; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -22,4 +25,10 @@ void checkCredentialsBatches() { void checkCredentialsSessions() { CredentialsValidationHelper.checkCredentials(() -> httpClient, VoiceService::sessions); } + + @Test + void sinchEventsRequireNoCredentials() { + Assertions.assertNotNull( + new SinchClient(Configuration.builder().build()).voice().v2().sinchEvents()); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/WebhooksHelper.java b/client/src/test/java/com/sinch/sdk/e2e/domains/WebhooksHelper.java index 45deaf9df..089b8e655 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/WebhooksHelper.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/WebhooksHelper.java @@ -3,8 +3,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,6 +37,19 @@ public static Response callURL(URL url, Function parseEvent) return response; } + /** Send a response payload back to the mock server; returns the HTTP status code */ + public static int postJson(URL url, String jsonPayload) throws IOException { + + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + con.setDoOutput(true); + try (OutputStream outputStream = con.getOutputStream()) { + outputStream.write(jsonPayload.getBytes(StandardCharsets.UTF_8)); + } + return con.getResponseCode(); + } + static Map transformHeaders(Map> headers) { if (null == headers) { return null; diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SinchEventsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SinchEventsSteps.java new file mode 100644 index 000000000..05eb0fcd1 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SinchEventsSteps.java @@ -0,0 +1,166 @@ +package com.sinch.sdk.e2e.domains.voice.v2; + +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.api.v2.SinchEventsService; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.CallDirection; +import com.sinch.sdk.domains.voice.models.v2.CallResult; +import com.sinch.sdk.domains.voice.models.v2.CallType; +import com.sinch.sdk.domains.voice.models.v2.Money; +import com.sinch.sdk.domains.voice.models.v2.OriginationType; +import com.sinch.sdk.domains.voice.models.v2.destination.Phone; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.SinchEventType; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage; +import com.sinch.sdk.e2e.Config; +import com.sinch.sdk.e2e.domains.WebhooksHelper; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.io.IOException; +import java.net.URL; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Assertions; + +public class SinchEventsSteps { + + static final String SERVICE_ID = "serviceKey"; + static final String SERVICE_SECRET = "BeIukql3pTKJ8RGL5zo0DA=="; + static final String WEBHOOKS_PATH_PREFIX = "/webhooks/voice-v2"; + + SinchEventsService service; + + Map> receivedEvents = new HashMap<>(); + + static Call.Builder baseCall() { + return Call.builder() + .setCallId("01HZXK7QNPMR8VD3JW9YF2C4CA") + .setServiceId("f9e8d7c6-b5a4-4321-9876-c5d4e3f2a1b0") + .setProjectId("a1b2c3d4-e5f6-4789-a012-b3c4d5e6f789") + .setSessionId("01HZXK7QNPMR8VD3JW9YF2C4TB") + .setCallType(CallType.PHONE) + .setStartTime(Instant.parse("2026-09-04T10:14:55Z")) + .setCallRate(Money.builder().setCurrencyCode("EUR").setAmount("0.0095").build()) + .setCallResourceUrl( + "/v2/projects/a1b2c3d4-e5f6-4789-a012-b3c4d5e6f789/calls/01HZXK7QNPMR8VD3JW9YF2C4CA"); + } + + static final Call incomingCall = + baseCall() + .setFrom(Phone.builder().setNumber("+12015555555").build()) + .setTo(Phone.builder().setNumber("+12017777777").build()) + .setDirection(CallDirection.INBOUND) + .setCallResult(CallResult.INITIATED) + .setOriginationType(OriginationType.PHONE) + .build(); + + static final Call answeredCall = + baseCall() + .setFrom(Phone.builder().setNumber("+12017777777").build()) + .setTo(Phone.builder().setNumber("+12015555555").build()) + .setDirection(CallDirection.OUTBOUND) + .setCallResult(CallResult.IN_PROGRESS) + .setOriginationType(OriginationType.SERVER) + .setAnswerTime(Instant.parse("2026-09-04T10:14:58Z")) + .setUpdateTime(Instant.parse("2026-09-04T10:14:58Z")) + .build(); + + static final Map expectedEvents = new HashMap<>(); + + static { + expectedEvents.put( + "call.incoming", + VoiceSinchEvent.builder() + .setEvent(SinchEventType.CALL_INCOMING) + .setCall(incomingCall) + .build()); + expectedEvents.put( + "call.answered", + VoiceSinchEvent.builder() + .setEvent(SinchEventType.CALL_ANSWERED) + .setCall(answeredCall) + .build()); + expectedEvents.put( + "call.webhook.on-answer", + VoiceSinchEvent.builder() + .setEvent(SinchEventType.from("call.customEvent.on-answer")) + .setCall(answeredCall) + .build()); + } + + static final SvamlInput onAnswerResponse = + SvamlInput.builder() + .setCommands( + Collections.singletonList( + MessagesCommand.builder() + .setMessagesName("from-webhook-server") + .setMessages( + Collections.singletonList( + SayMessage.builder() + .setText("This message came from your local webhook server.") + .setVoiceName("Emma") + .build())) + .setEvents( + MessageEvents.builder() + .setOnFinish( + Collections.singletonList(HangupCommand.builder().build())) + .build()) + .build())) + .build(); + + // "call.webhook.on-answer" is served at "/webhooks/voice-v2/call/webhook/on-answer" + static String pathOf(String event) { + return WEBHOOKS_PATH_PREFIX + "/" + event.replace('.', '/'); + } + + @Given("^the Voice-V2 Webhooks handler is available$") + public void serviceAvailable() { + service = Config.getSinchClient().voice().v2().sinchEvents(); + } + + @When("I send a request to trigger a {string} event") + public void sendEvent(String event) throws IOException { + receivedEvents.put( + event, + WebhooksHelper.callURL( + new URL(Config.VOICE_V2_HOST_NAME + pathOf(event)), service::parseEvent)); + } + + @Then("the header of the {string} event contains a valid authorization") + public void validateHeader(String event) { + WebhooksHelper.Response received = receivedEvents.get(event); + + boolean validated = + service.validateAuthenticationHeader( + SERVICE_ID, + SERVICE_SECRET, + "POST", + pathOf(event), + received.headers, + received.rawPayload); + Assertions.assertTrue(validated); + } + + @Then("the Voice-V2 event describes a {string} event") + public void validateEvent(String event) { + TestHelpers.recursiveEquals(receivedEvents.get(event).event, expectedEvents.get(event)); + } + + @Then("the response to the {string} event matches the expected call control instructions") + public void validateResponse(String event) throws IOException { + Assertions.assertEquals("call.webhook.on-answer", event); + + int status = + WebhooksHelper.postJson( + new URL(Config.VOICE_V2_HOST_NAME + pathOf(event) + "/confirm"), + service.serializeResponse(onAnswerResponse)); + Assertions.assertEquals(200, status); + } +} diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index b9d2459da..ecc04c0dd 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -24,11 +24,13 @@ @SelectClasspathResource("features/voice-v2/calls.feature") @SelectClasspathResource("features/voice-v2/batches.feature") @SelectClasspathResource("features/voice-v2/sessions.feature") +@SelectClasspathResource("features/voice-v2/webhooks-events.feature") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.sinch.sdk.e2e.domains.voice.v2") @ConfigurationParameter( key = FILTER_NAME_PROPERTY_NAME, value = "^\\[Start\\].*|^\\[Get\\] get a batch call summary$|^\\[GetDetails\\] get batch call" - + " details$|^\\[Stop\\] stop batch processing$|^\\[Get\\] get a session$") + + " details$|^\\[Stop\\] stop batch processing$|^\\[Get\\] get a session$" + + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/sinch-events/README.md b/examples/sinch-events/README.md index 2d7f4e3f4..c4f54579f 100644 --- a/examples/sinch-events/README.md +++ b/examples/sinch-events/README.md @@ -27,10 +27,11 @@ Use these keys when request authentication is enabled in each controller (`ensur - `conversation.sinchevents.secret` for Conversation callbacks - `numbers.sinchevents.secret` for Numbers callbacks - `sms.sinchevents.secret` for SMS callbacks +- `voice.v2.sinchevents.service-id` and `voice.v2.sinchevents.service-secret` for Voice V2 callbacks: the ID and secret of the service the events are sent for ### Voice and Verification authentication -Voice and Verification callback validation uses application credentials: +Voice (V1) and Verification callback validation uses application credentials: - `credentials.application-api-key` - `credentials.application-api-secret` @@ -65,6 +66,7 @@ When the server is running, controllers listen on: | [SMS](https://developers.sinch.com/docs/sms/api-reference/sms/tag/Webhooks/#tag/Webhooks/section/Callbacks) | `/SmsEvent` | | [Verification](https://developers.sinch.com/docs/verification/api-reference/verification/tag/Verification-callbacks/) | `/VerificationEvent` | | [Voice](https://developers.sinch.com/docs/voice/api-reference/voice/tag/Callbacks/) | `/VoiceEvent` | +| [Voice V2](https://developers.sinch.com/docs/voice-2.0) | `/VoiceV2Event` | ## Expose local server with ngrok @@ -89,5 +91,6 @@ Example callback URLs: - `https:///SmsEvent` - `https:///VerificationEvent` - `https:///VoiceEvent` +- `https:///VoiceV2Event` Dashboard: [Sinch Dashboard](https://dashboard.sinch.com) diff --git a/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/Controller.java b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/Controller.java new file mode 100644 index 000000000..63301988e --- /dev/null +++ b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/Controller.java @@ -0,0 +1,104 @@ +package com.mycompany.app.voice.v2; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.SinchEventsService; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.SinchEventType; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import java.util.Map; +import java.util.logging.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@RestController("VoiceV2") +public class Controller { + + private static final Logger LOGGER = Logger.getLogger(Controller.class.getName()); + + private final SinchClient sinchClient; + private final ServerBusinessLogic serverBusinessLogic; + + @Value("${voice.v2.sinchevents.service-id: }") + private String serviceId; + + @Value("${voice.v2.sinchevents.service-secret: }") + private String serviceSecret; + + @Autowired + public Controller(SinchClient sinchClient, ServerBusinessLogic serverBusinessLogic) { + this.sinchClient = sinchClient; + this.serverBusinessLogic = serverBusinessLogic; + } + + @PostMapping( + value = "/VoiceV2Event", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity voiceV2Event( + @RequestHeader Map headers, @RequestBody String body) { + + // Sinch Events do not need any credentials from the client configuration + SinchEventsService sinchEvents = sinchClient.voice().v2().sinchEvents(); + + // ensure valid authentication to handle request + // set this value to true to validate request from Sinch servers: requests are signed with the + // secret of the service the event belongs to ("Authorization: service :") + boolean ensureValidAuthentication = false; + if (ensureValidAuthentication) { + var validAuth = + sinchEvents.validateAuthenticationHeader( + // The service the events are sent for + serviceId, + // The secret of this service + serviceSecret, + // The HTTP verb this controller is managing + "POST", + // The URI this controller is managing + "/VoiceV2Event", + // request headers + headers, + // request payload body + body); + + // signature validation failed + if (!validAuth) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); + } + + // The SDK does not protect against replayed requests: reject here any request whose + // "x-timestamp" header is outside the clock-skew window your application accepts + } + + // decode the payload request + VoiceSinchEvent event = sinchEvents.parseEvent(body); + + // let business layer process the request + SinchEventType type = event.getEvent(); + SvamlInput response; + if (SinchEventType.CALL_INCOMING.equals(type)) { + response = serverBusinessLogic.incoming(event); + } else if (SinchEventType.CALL_ANSWERED.equals(type)) { + response = serverBusinessLogic.answered(event); + } else if (SinchEventType.CALL_MENU.equals(type)) { + response = serverBusinessLogic.menu(event); + } else if (SinchEventType.valueOf(type).startsWith("call.customEvent.")) { + // dynamic events, triggered by the "customEvent" SVAML command + response = serverBusinessLogic.customEvent(event); + } else { + response = serverBusinessLogic.other(event); + } + + // the Voice Platform expects a SVAML document in response to every event + var serializedResponse = sinchEvents.serializeResponse(response); + LOGGER.finest("JSON response: " + serializedResponse); + return ResponseEntity.ok(serializedResponse); + } +} diff --git a/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java new file mode 100644 index 000000000..01e50e4dc --- /dev/null +++ b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java @@ -0,0 +1,75 @@ +package com.mycompany.app.voice.v2; + +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage; +import java.util.Collections; +import java.util.logging.Logger; +import org.springframework.stereotype.Component; + +@Component("VoiceV2ServerBusinessLogic") +public class ServerBusinessLogic { + + private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); + + // An empty list of commands takes no action + private static final SvamlInput NO_ACTION = + SvamlInput.builder().setCommands(Collections.emptyList()).build(); + + public SvamlInput incoming(VoiceSinchEvent event) { + + LOGGER.info("Handle event: " + event); + + // play a greeting, then hang up + // "callName" and "events" only take effect in responses to "call.incoming" events + return SvamlInput.builder() + .setCallName("incoming") + .setCommands( + Collections.singletonList( + MessagesCommand.builder() + .setMessages( + Collections.singletonList( + SayMessage.builder() + .setText("Thank you for calling. Goodbye.") + .setVoiceName("Emma") + .build())) + .setEvents( + MessageEvents.builder() + .setOnFinish(Collections.singletonList(HangupCommand.builder().build())) + .build()) + .build())) + .build(); + } + + public SvamlInput answered(VoiceSinchEvent event) { + + LOGGER.info("Handle event: " + event); + + return NO_ACTION; + } + + public SvamlInput menu(VoiceSinchEvent event) { + + // event.getMenu() holds the menu name and the input gathered from the user + LOGGER.info("Handle event: " + event); + + return NO_ACTION; + } + + public SvamlInput customEvent(VoiceSinchEvent event) { + + LOGGER.info("Handle event: " + event); + + return NO_ACTION; + } + + public SvamlInput other(VoiceSinchEvent event) { + + LOGGER.info("Handle event: " + event); + + return NO_ACTION; + } +} diff --git a/examples/sinch-events/src/main/resources/application.yaml b/examples/sinch-events/src/main/resources/application.yaml index 9b8a23005..08b883e6d 100644 --- a/examples/sinch-events/src/main/resources/application.yaml +++ b/examples/sinch-events/src/main/resources/application.yaml @@ -50,3 +50,11 @@ sms: # Secret value set for Sms Sinch Events calls validation # Used when "ensureValidAuthentication" within [Controller.java](../java/com/mycompany/app/sms/Controller.java) is set to true secret: + +voice: + v2: + sinchevents: + # Service ID and secret used for Voice V2 Sinch Events calls validation + # Used when "ensureValidAuthentication" within [Controller.java](../java/com/mycompany/app/voice/v2/Controller.java) is set to true + service-id: + service-secret: diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInput.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInput.java new file mode 100644 index 000000000..6c296cc81 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInput.java @@ -0,0 +1,70 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * Information about the menu interaction that triggered the Sinch Event, including the menu name + * and the input sequence received from the user. + * + *

This property is also included for Sinch Events triggered by the webhook command + * within a menu context. + */ +@JsonDeserialize(builder = MenuInputImpl.Builder.class) +public interface MenuInput { + + /** + * The name of the menu that triggered this Sinch Event. + * + *

Field is required + * + * @return menuName + */ + String getMenuName(); + + /** + * The input sequence gathered from the user by the menu. + * + *

Field is required + * + * @return input + */ + String getInput(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new MenuInputImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param menuName see getter + * @return Current builder + * @see #getMenuName + */ + Builder setMenuName(String menuName); + + /** + * see getter + * + * @param input see getter + * @return Current builder + * @see #getInput + */ + Builder setInput(String input); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + MenuInput build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInputImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInputImpl.java new file mode 100644 index 000000000..7c306949f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInputImpl.java @@ -0,0 +1,115 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({MenuInputImpl.JSON_PROPERTY_MENU_NAME, MenuInputImpl.JSON_PROPERTY_INPUT}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class MenuInputImpl implements MenuInput { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_MENU_NAME = "menuName"; + + private OptionalValue menuName; + + public static final String JSON_PROPERTY_INPUT = "input"; + + private OptionalValue input; + + public MenuInputImpl() {} + + protected MenuInputImpl(OptionalValue menuName, OptionalValue input) { + this.menuName = menuName; + this.input = input; + } + + @JsonIgnore + public String getMenuName() { + return menuName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MENU_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue menuName() { + return menuName; + } + + @JsonIgnore + public String getInput() { + return input.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_INPUT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue input() { + return input; + } + + /** Return true if this MenuInput object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MenuInputImpl menuInput = (MenuInputImpl) o; + return Objects.equals(this.menuName, menuInput.menuName) + && Objects.equals(this.input, menuInput.input); + } + + @Override + public int hashCode() { + return Objects.hash(menuName, input); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class MenuInputImpl {\n"); + sb.append(" menuName: ").append(toIndentedString(menuName)).append("\n"); + sb.append(" input: ").append(toIndentedString(input)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements MenuInput.Builder { + OptionalValue menuName = OptionalValue.empty(); + OptionalValue input = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_MENU_NAME, required = true) + public Builder setMenuName(String menuName) { + this.menuName = OptionalValue.of(menuName); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_INPUT, required = true) + public Builder setInput(String input) { + this.input = OptionalValue.of(input); + return this; + } + + public MenuInput build() { + return new MenuInputImpl(menuName, input); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/SinchEventType.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/SinchEventType.java new file mode 100644 index 000000000..275677be0 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/SinchEventType.java @@ -0,0 +1,110 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Identifies the type of call event that triggered this Sinch Event notification. + * + *

Call-related events always start with call. followed by the type of event that + * triggered them. Events triggered by the {@link + * com.sinch.sdk.domains.voice.models.v2.svaml.customevents.CustomEventCommand} are dynamic and + * follow the pattern call.customEvent.<customEventName>: they are not listed as + * constants and are returned as a dynamic value. The API sends them as + * call.webhook.<customEventName>, and parseEvent renames them. + */ +public class SinchEventType extends EnumDynamic { + + /** Triggered when an inbound call arrives on a service configured with a webhook URL. */ + public static final SinchEventType CALL_INCOMING = new SinchEventType("call.incoming"); + + /** Triggered when an outbound call is answered by the recipient. */ + public static final SinchEventType CALL_ANSWERED = new SinchEventType("call.answered"); + + /** Triggered when the outbound call recipient is busy. */ + public static final SinchEventType CALL_BUSY = new SinchEventType("call.busy"); + + /** Triggered when the outbound call is rejected by the recipient. */ + public static final SinchEventType CALL_REJECTED = new SinchEventType("call.rejected"); + + /** Triggered when the outbound call was not answered within the dial timeout. */ + public static final SinchEventType CALL_TIMEOUT = new SinchEventType("call.timeout"); + + /** Triggered when the call is disconnected. */ + public static final SinchEventType CALL_HANGUP = new SinchEventType("call.hangup"); + + /** Triggered when the call could not be set up. */ + public static final SinchEventType CALL_FAILED = new SinchEventType("call.failed"); + + /** Triggered when answering machine detection determines that a human answered the call. */ + public static final SinchEventType CALL_AMD_HUMAN = new SinchEventType("call.amd.human"); + + /** Triggered when answering machine detection determines that a machine answered the call. */ + public static final SinchEventType CALL_AMD_MACHINE = new SinchEventType("call.amd.machine"); + + /** Triggered when answering machine detection detects a voicemail beep. */ + public static final SinchEventType CALL_AMD_BEEP = new SinchEventType("call.amd.beep"); + + /** + * Triggered when answering machine detection cannot determine whether a human, machine, or beep + * was detected. + */ + public static final SinchEventType CALL_AMD_UNKNOWN = new SinchEventType("call.amd.unknown"); + + /** Triggered when all messages in a messages sequence have finished playing. */ + public static final SinchEventType CALL_MESSAGE_FINISHED = + new SinchEventType("call.message.finished"); + + /** + * Triggered when a recording is successfully stopped. This event does not guarantee that the + * recorded file has been delivered to the configured destination yet. + */ + public static final SinchEventType CALL_RECORDING_FINISHED = + new SinchEventType("call.recording.finished"); + + /** Triggered when recording could not be started. */ + public static final SinchEventType CALL_RECORDING_FAILED = + new SinchEventType("call.recording.failed"); + + /** Triggered when a menu completes and input is received from the user or the menu fails. */ + public static final SinchEventType CALL_MENU = new SinchEventType("call.menu"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + SinchEventType.class, + SinchEventType::new, + Arrays.asList( + CALL_INCOMING, + CALL_ANSWERED, + CALL_BUSY, + CALL_REJECTED, + CALL_TIMEOUT, + CALL_HANGUP, + CALL_FAILED, + CALL_AMD_HUMAN, + CALL_AMD_MACHINE, + CALL_AMD_BEEP, + CALL_AMD_UNKNOWN, + CALL_MESSAGE_FINISHED, + CALL_RECORDING_FINISHED, + CALL_RECORDING_FAILED, + CALL_MENU)); + + private SinchEventType(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static SinchEventType from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(SinchEventType e) { + return ENUM_SUPPORT.valueOf(e); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEvent.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEvent.java new file mode 100644 index 000000000..b30ba66e4 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEvent.java @@ -0,0 +1,87 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.Call; + +/** The event payload delivered by the Voice Platform to the Sinch Events endpoint. */ +@JsonDeserialize(builder = VoiceSinchEventImpl.Builder.class) +public interface VoiceSinchEvent { + + /** + * Identifies the type of call event that triggered this Sinch Event notification. + * + *

Call-related events always start with call. followed by the type of event that + * triggered them. Events triggered by the customEvent SVAML command are dynamic and + * follow the pattern call.customEvent.<customEventName>. + * + *

Field is required + * + * @return event + */ + SinchEventType getEvent(); + + /** + * The current state of the call at the time the event was triggered. + * + *

Field is required + * + * @return call + */ + Call getCall(); + + /** + * Information about the menu interaction that triggered the Sinch Event. Present for + * call.menu events, and for call.customEvent.<customEventName> events + * triggered within a menu context. + * + * @return menu + */ + MenuInput getMenu(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new VoiceSinchEventImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param event see getter + * @return Current builder + * @see #getEvent + */ + Builder setEvent(SinchEventType event); + + /** + * see getter + * + * @param call see getter + * @return Current builder + * @see #getCall + */ + Builder setCall(Call call); + + /** + * see getter + * + * @param menu see getter + * @return Current builder + * @see #getMenu + */ + Builder setMenu(MenuInput menu); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + VoiceSinchEvent build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventImpl.java new file mode 100644 index 000000000..851b68dc5 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventImpl.java @@ -0,0 +1,148 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.Call; +import java.util.Objects; + +@JsonPropertyOrder({ + VoiceSinchEventImpl.JSON_PROPERTY_EVENT, + VoiceSinchEventImpl.JSON_PROPERTY_CALL, + VoiceSinchEventImpl.JSON_PROPERTY_MENU +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class VoiceSinchEventImpl implements VoiceSinchEvent { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_EVENT = "event"; + + private OptionalValue event; + + public static final String JSON_PROPERTY_CALL = "call"; + + private OptionalValue call; + + public static final String JSON_PROPERTY_MENU = "menu"; + + private OptionalValue menu; + + public VoiceSinchEventImpl() {} + + protected VoiceSinchEventImpl( + OptionalValue event, + OptionalValue call, + OptionalValue menu) { + this.event = event; + this.call = call; + this.menu = menu; + } + + @JsonIgnore + public SinchEventType getEvent() { + return event.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_EVENT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue event() { + return event; + } + + @JsonIgnore + public Call getCall() { + return call.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue call() { + return call; + } + + @JsonIgnore + public MenuInput getMenu() { + return menu.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_MENU) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue menu() { + return menu; + } + + /** Return true if this VoiceSinchEvent object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VoiceSinchEventImpl voiceSinchEvent = (VoiceSinchEventImpl) o; + return Objects.equals(this.event, voiceSinchEvent.event) + && Objects.equals(this.call, voiceSinchEvent.call) + && Objects.equals(this.menu, voiceSinchEvent.menu); + } + + @Override + public int hashCode() { + return Objects.hash(event, call, menu); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class VoiceSinchEventImpl {\n"); + sb.append(" event: ").append(toIndentedString(event)).append("\n"); + sb.append(" call: ").append(toIndentedString(call)).append("\n"); + sb.append(" menu: ").append(toIndentedString(menu)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements VoiceSinchEvent.Builder { + OptionalValue event = OptionalValue.empty(); + OptionalValue call = OptionalValue.empty(); + OptionalValue menu = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_EVENT, required = true) + public Builder setEvent(SinchEventType event) { + this.event = OptionalValue.of(event); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CALL, required = true) + public Builder setCall(Call call) { + this.call = OptionalValue.of(call); + return this; + } + + @JsonProperty(JSON_PROPERTY_MENU) + public Builder setMenu(MenuInput menu) { + this.menu = OptionalValue.of(menu); + return this; + } + + public VoiceSinchEvent build() { + return new VoiceSinchEventImpl(event, call, menu); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java new file mode 100644 index 000000000..0a8cb953f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java @@ -0,0 +1,99 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** + * A SVAML v2 (Sinch Voice Application Markup Language) document describing a call flow. Contains + * the ordered list of commands to execute, an optional call name, and optional event handlers. + * + *

When returned in response to a Sinch Event, callName and events only + * take effect in responses to events triggered by an incoming call. In responses to other event + * types, they are ignored. + */ +@JsonDeserialize(builder = SvamlInputImpl.Builder.class) +public interface SvamlInput { + + /** + * The ordered list of SVAML commands to execute. Contains at least one command. + * + *

Blocking vs. non-blocking: Some commands block execution until they complete (pause + * , webhook, menu, gotoMenu), while others return + * immediately and run in parallel (dial, messages, amd, + * answer, hangup, startRecording, stopRecording + * , bridgeCall, stopMessages). Each command's description + * specifies its behavior. + * + *

Nesting scope: Commands that appear inside event handlers (e.g., dial.events.onAnswer + * , messages.events.onFinish) form independent sequences and execute in their + * own context — they are not continuations of the parent sequence. + * + *

Field is required + * + * @return commands + */ + List getCommands(); + + /** + * Name of the call. + * + *

Must be 1-32 characters, without whitespace. + * + * @return callName + */ + String getCallName(); + + /** + * Commands to execute on specific events for this call. + * + * @return events + */ + SvamlInputEvents getEvents(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SvamlInputImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param commands see getter + * @return Current builder + * @see #getCommands + */ + Builder setCommands(List commands); + + /** + * see getter + * + * @param callName see getter + * @return Current builder + * @see #getCallName + */ + Builder setCallName(String callName); + + /** + * see getter + * + * @param events see getter + * @return Current builder + * @see #getEvents + */ + Builder setEvents(SvamlInputEvents events); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + SvamlInput build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEvents.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEvents.java new file mode 100644 index 000000000..15056781c --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEvents.java @@ -0,0 +1,45 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** Commands to execute on specific events for this call. */ +@JsonDeserialize(builder = SvamlInputEventsImpl.Builder.class) +public interface SvamlInputEvents { + + /** + * SVAML commands to be executed when the call is hung up. + * + * @return onHangup + */ + List getOnHangup(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SvamlInputEventsImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param onHangup see getter + * @return Current builder + * @see #getOnHangup + */ + Builder setOnHangup(List onHangup); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + SvamlInputEvents build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsImpl.java new file mode 100644 index 000000000..cf90b9e09 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsImpl.java @@ -0,0 +1,91 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({SvamlInputEventsImpl.JSON_PROPERTY_ON_HANGUP}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SvamlInputEventsImpl implements SvamlInputEvents { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_ON_HANGUP = "onHangup"; + + private OptionalValue> onHangup; + + public SvamlInputEventsImpl() {} + + protected SvamlInputEventsImpl(OptionalValue> onHangup) { + this.onHangup = onHangup; + } + + @JsonIgnore + public List getOnHangup() { + return onHangup.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ON_HANGUP) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> onHangup() { + return onHangup; + } + + /** Return true if this SvamlInputEvents object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SvamlInputEventsImpl svamlInputEvents = (SvamlInputEventsImpl) o; + return Objects.equals(this.onHangup, svamlInputEvents.onHangup); + } + + @Override + public int hashCode() { + return Objects.hash(onHangup); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SvamlInputEventsImpl {\n"); + sb.append(" onHangup: ").append(toIndentedString(onHangup)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements SvamlInputEvents.Builder { + OptionalValue> onHangup = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_ON_HANGUP) + public Builder setOnHangup(List onHangup) { + this.onHangup = OptionalValue.of(onHangup); + return this; + } + + public SvamlInputEvents build() { + return new SvamlInputEventsImpl(onHangup); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputImpl.java new file mode 100644 index 000000000..ae2aa3577 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputImpl.java @@ -0,0 +1,148 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + SvamlInputImpl.JSON_PROPERTY_COMMANDS, + SvamlInputImpl.JSON_PROPERTY_CALL_NAME, + SvamlInputImpl.JSON_PROPERTY_EVENTS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SvamlInputImpl implements SvamlInput { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMANDS = "commands"; + + private OptionalValue> commands; + + public static final String JSON_PROPERTY_CALL_NAME = "callName"; + + private OptionalValue callName; + + public static final String JSON_PROPERTY_EVENTS = "events"; + + private OptionalValue events; + + public SvamlInputImpl() {} + + protected SvamlInputImpl( + OptionalValue> commands, + OptionalValue callName, + OptionalValue events) { + this.commands = commands; + this.callName = callName; + this.events = events; + } + + @JsonIgnore + public List getCommands() { + return commands.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMANDS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> commands() { + return commands; + } + + @JsonIgnore + public String getCallName() { + return callName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callName() { + return callName; + } + + @JsonIgnore + public SvamlInputEvents getEvents() { + return events.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue events() { + return events; + } + + /** Return true if this SvamlInput object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SvamlInputImpl svamlInput = (SvamlInputImpl) o; + return Objects.equals(this.commands, svamlInput.commands) + && Objects.equals(this.callName, svamlInput.callName) + && Objects.equals(this.events, svamlInput.events); + } + + @Override + public int hashCode() { + return Objects.hash(commands, callName, events); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SvamlInputImpl {\n"); + sb.append(" commands: ").append(toIndentedString(commands)).append("\n"); + sb.append(" callName: ").append(toIndentedString(callName)).append("\n"); + sb.append(" events: ").append(toIndentedString(events)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements SvamlInput.Builder { + OptionalValue> commands = OptionalValue.empty(); + OptionalValue callName = OptionalValue.empty(); + OptionalValue events = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMANDS, required = true) + public Builder setCommands(List commands) { + this.commands = OptionalValue.of(commands); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + public Builder setCallName(String callName) { + this.callName = OptionalValue.of(callName); + return this; + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + public Builder setEvents(SvamlInputEvents events) { + this.events = OptionalValue.of(events); + return this; + } + + public SvamlInput build() { + return new SvamlInputImpl(commands, callName, events); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInputDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInputDtoTest.java new file mode 100644 index 000000000..9e6e6a3e3 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/MenuInputDtoTest.java @@ -0,0 +1,25 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class MenuInputDtoTest extends BaseTest { + + public static final MenuInput expectedMenuInput = + MenuInput.builder().setMenuName("main").setInput("1234").build(); + + @GivenTextResource("/domains/voice/v2/sinchevents/MenuInputDto.json") + String jsonMenuInput; + + @Test + void deserialize() throws JsonProcessingException { + MenuInput deserialized = objectMapper.readValue(jsonMenuInput, MenuInput.class); + + TestHelpers.recursiveEquals(deserialized, expectedMenuInput); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventDtoTest.java new file mode 100644 index 000000000..412d1d681 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventDtoTest.java @@ -0,0 +1,51 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.CallDtoTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class VoiceSinchEventDtoTest extends BaseTest { + + public static final VoiceSinchEvent expectedVoiceSinchEvent = + VoiceSinchEvent.builder() + .setEvent(SinchEventType.CALL_MENU) + .setCall(CallDtoTest.expectedCall) + .setMenu(MenuInputDtoTest.expectedMenuInput) + .build(); + + public static final VoiceSinchEvent expectedVoiceSinchEventCustomEvent = + VoiceSinchEvent.builder() + .setEvent(SinchEventType.from("call.webhook.on-answer")) + .setCall(CallDtoTest.expectedCall) + .build(); + + @GivenTextResource("/domains/voice/v2/sinchevents/VoiceSinchEventDto.json") + String jsonVoiceSinchEvent; + + @GivenTextResource("/domains/voice/v2/sinchevents/VoiceSinchEventCustomEventDto.json") + String jsonVoiceSinchEventCustomEvent; + + @Test + void deserialize() throws JsonProcessingException { + VoiceSinchEvent deserialized = + objectMapper.readValue(jsonVoiceSinchEvent, VoiceSinchEvent.class); + + TestHelpers.recursiveEquals(deserialized, expectedVoiceSinchEvent); + } + + @Test + void deserializeCustomEvent() throws JsonProcessingException { + VoiceSinchEvent deserialized = + objectMapper.readValue(jsonVoiceSinchEventCustomEvent, VoiceSinchEvent.class); + + TestHelpers.recursiveEquals(deserialized, expectedVoiceSinchEventCustomEvent); + Assertions.assertThat(SinchEventType.valueOf(deserialized.getEvent())) + .isEqualTo("call.webhook.on-answer"); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputDtoTest.java new file mode 100644 index 000000000..f54b4ef2a --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputDtoTest.java @@ -0,0 +1,32 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommandDtoTest; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SvamlInputDtoTest extends BaseTest { + + public static final SvamlInput expectedSvamlInput = + SvamlInput.builder() + .setCommands(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .setCallName("incoming") + .setEvents(SvamlInputEventsDtoTest.expectedSvamlInputEvents) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/SvamlInputDto.json") + String jsonSvamlInput; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSvamlInput); + + JSONAssert.assertEquals(jsonSvamlInput, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsDtoTest.java new file mode 100644 index 000000000..3bb1ee4a2 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsDtoTest.java @@ -0,0 +1,30 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommandDtoTest; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class SvamlInputEventsDtoTest extends BaseTest { + + public static final SvamlInputEvents expectedSvamlInputEvents = + SvamlInputEvents.builder() + .setOnHangup(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/SvamlInputEventsDto.json") + String jsonSvamlInputEvents; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedSvamlInputEvents); + + JSONAssert.assertEquals(jsonSvamlInputEvents, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/MenuInputDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/MenuInputDto.json new file mode 100644 index 000000000..483f86b8d --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/MenuInputDto.json @@ -0,0 +1,4 @@ +{ + "menuName": "main", + "input": "1234" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventCustomEventDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventCustomEventDto.json new file mode 100644 index 000000000..9e6b4a42f --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventCustomEventDto.json @@ -0,0 +1,40 @@ +{ + "event": "call.webhook.on-answer", + "call": { + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "callName": "origin", + "bridgeName": "my-bridge", + "batchId": "01BX5ZZKBKACTAV9WEVGEMMVRC", + "from": { + "type": "SIP", + "sip": { + "endpoint": "sip:alice@sip.example.com", + "displayName": "Alice" + } + }, + "to": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "startTime": "2025-02-10T09:00:00Z", + "updateTime": "2025-02-10T09:00:47Z", + "callType": "PHONE", + "direction": "INBOUND", + "answerTime": "2025-02-10T09:00:05Z", + "endTime": "2025-02-10T09:00:47Z", + "callDurationSeconds": 42, + "callResult": "COMPLETED", + "callReason": "CALLEE_HANGUP", + "originationType": "SIP", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventDto.json new file mode 100644 index 000000000..70982cb35 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventDto.json @@ -0,0 +1,44 @@ +{ + "event": "call.menu", + "call": { + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "callName": "origin", + "bridgeName": "my-bridge", + "batchId": "01BX5ZZKBKACTAV9WEVGEMMVRC", + "from": { + "type": "SIP", + "sip": { + "endpoint": "sip:alice@sip.example.com", + "displayName": "Alice" + } + }, + "to": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "startTime": "2025-02-10T09:00:00Z", + "updateTime": "2025-02-10T09:00:47Z", + "callType": "PHONE", + "direction": "INBOUND", + "answerTime": "2025-02-10T09:00:05Z", + "endTime": "2025-02-10T09:00:47Z", + "callDurationSeconds": 42, + "callResult": "COMPLETED", + "callReason": "CALLEE_HANGUP", + "originationType": "SIP", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" + }, + "menu": { + "menuName": "main", + "input": "1234" + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputDto.json new file mode 100644 index 000000000..7f6fd301e --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputDto.json @@ -0,0 +1,17 @@ +{ + "commands": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "callName": "incoming", + "events": { + "onHangup": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputEventsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputEventsDto.json new file mode 100644 index 000000000..236ffa0f4 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputEventsDto.json @@ -0,0 +1,8 @@ +{ + "onHangup": [ + { + "command": "hangup", + "callName": "origin" + } + ] +} From c1cc1512a3f7c105cdbba7b840cc57e464580883 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 02:10:57 +0200 Subject: [PATCH 26/50] Add Voice v2 calls.get endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/CallsServiceTest.java | 58 +++++++++++++++++ .../sdk/e2e/domains/voice/v2/CallsSteps.java | 42 ++++++++++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 5 +- .../snippets/src/main/java/voice/README.md | 1 + .../src/main/java/voice/v2/calls/Get.java | 47 ++++++++++++++ .../domains/voice/api/v2/CallsService.java | 13 ++++ .../api/v2/adapters/CallsServiceImpl.java | 65 +++++++++++++++++++ 8 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 examples/snippets/src/main/java/voice/v2/calls/Get.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e69b6ba9f..83b132b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. ### Voice - **[feature]** Support [Voice API v2](https://developers.sinch.com/docs/voice-2.0), exposed under `voice().v2()` - **[feature]** Voice V2 Calls API: `start` operation to create an outbound call +- **[feature]** Voice V2 Calls API: `get` operation to retrieve a call - **[feature]** Voice V2 Batches API: `start` operation to create a batch of outbound calls - **[feature]** Voice V2 Batches API: `get` operation to retrieve a batch summary - **[feature]** Voice V2 Batches API: `getDetails` operation to retrieve the per-session details of a batch @@ -27,6 +28,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Sessions API: `get` operation to retrieve a session and its calls - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet +- **[doc]** New `voice/v2/calls/Get` snippet - **[doc]** New `voice/v2/batches/Start` snippet - **[doc]** New `voice/v2/batches/Get` snippet - **[doc]** New `voice/v2/batches/GetDetails` snippet diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java index c6599d752..183ce189c 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java @@ -21,6 +21,8 @@ import com.sinch.sdk.core.http.URLPathUtils; import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.CallDtoTest; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequestDtoTest; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; @@ -54,6 +56,9 @@ public class CallsServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/calls/response/StartCallResponseDto.json") String jsonStartCallResponseDto; + @GivenTextResource("/domains/voice/v2/CallDto.json") + String jsonCallDto; + @BeforeEach public void initMocks() { service = @@ -119,4 +124,57 @@ void startMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + + @Test + void get() throws ApiException { + + String callId = "01ARZ3NDEKTSV4RRFFQ69G5FAA"; + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/calls/" + + URLPathUtils.encodePathSegment(callId), + HttpMethod.GET, + Collections.emptyList(), + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonCallDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + Call response = service.get(callId); + + TestHelpers.recursiveEquals(response, CallDtoTest.expectedCall); + } + + @Test + void getMissingCallIdThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.get(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void getMissingProjectIdThrows() { + + CallsService serviceWithoutProjectId = + new CallsServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, () -> serviceWithoutProjectId.get("01ARZ3NDEKTSV4RRFFQ69G5FAA")); + + Assertions.assertEquals(400, thrown.getCode()); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java index 6ef4ab484..4a7abc62b 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java @@ -2,6 +2,13 @@ import com.sinch.sdk.core.TestHelpers; import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.CallDirection; +import com.sinch.sdk.domains.voice.models.v2.CallReason; +import com.sinch.sdk.domains.voice.models.v2.CallResult; +import com.sinch.sdk.domains.voice.models.v2.CallType; +import com.sinch.sdk.domains.voice.models.v2.Money; +import com.sinch.sdk.domains.voice.models.v2.OriginationType; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; import com.sinch.sdk.domains.voice.models.v2.destination.Phone; @@ -16,6 +23,7 @@ import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Assertions; @@ -24,6 +32,7 @@ public class CallsSteps { CallsService service; StartCallResponse startResponse; + Call getResponse; @Given("^the Voice-V2 service \"Calls\" is available$") public void serviceAvailable() { @@ -83,4 +92,37 @@ public void startResult() { TestHelpers.recursiveEquals(startResponse, expected); } + + @When("^I send a request to get call details$") + public void get() { + getResponse = service.get("01HZXK8FQNPMR8VD3JW9YF2C5A"); + } + + @Then("^the response contains the call details$") + public void getResult() { + Call expected = + Call.builder() + .setCallId("01HZXK8FQNPMR8VD3JW9YF2C5A") + .setServiceId("f9e8d7c6-b5a4-4321-9876-c5d4e3f2a1b0") + .setProjectId("a1b2c3d4-e5f6-4789-a012-b3c4d5e6f789") + .setCallName("audio-notification") + .setSessionId("01HZXK7QNPMR8VD3JW9YF2C4TB") + .setFrom(Phone.builder().setNumber("+12015555555").build()) + .setTo(Phone.builder().setNumber("+12017777777").build()) + .setDirection(CallDirection.OUTBOUND) + .setCallResult(CallResult.NO_ANSWER) + .setCallType(CallType.PHONE) + .setOriginationType(OriginationType.SERVER) + .setStartTime(Instant.parse("2026-09-14T10:24:34Z")) + .setEndTime(Instant.parse("2026-09-14T10:24:49Z")) + .setUpdateTime(Instant.parse("2026-09-14T10:24:49Z")) + .setCallDurationSeconds(0) + .setCallRate(Money.builder().setCurrencyCode("EUR").setAmount("0.4085").build()) + .setCallReason(CallReason.NOT_AVAILABLE) + .setCallResourceUrl( + "https://eu1.voice.api.sinch.com/v2/projects/a1b2c3d4-e5f6-4789-a012-b3c4d5e6f789/calls/01HZXK8FQNPMR8VD3JW9YF2C5A") + .build(); + + TestHelpers.recursiveEquals(getResponse, expected); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index ecc04c0dd..7500f812a 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -15,8 +15,8 @@ * as the other Voice V2 operations land. * *

The filter matches scenario names across all features, and they share the same {@code [Get]} - * prefix. Batches' and sessions' {@code [Get]} are therefore matched by their full name, so that - * the calls {@code [Get]} scenario (not implemented yet) stays out. + * prefix. Each {@code [Get]} is therefore matched by its full name, so that a {@code [Get]} + * scenario of an operation not implemented yet stays out. */ @Suite @SuiteDisplayName("Voice V2") @@ -31,6 +31,7 @@ value = "^\\[Start\\].*|^\\[Get\\] get a batch call summary$|^\\[GetDetails\\] get batch call" + " details$|^\\[Stop\\] stop batch processing$|^\\[Get\\] get a session$" + + "|^\\[Get\\] get call details$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index beaf4c34f..9954f84cb 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -25,6 +25,7 @@ See main [README.md](../../../../README.md) for how to execute snippets - V2 - Calls - [voice/v2/calls/Start](./v2/calls/Start.java) + - [voice/v2/calls/Get](./v2/calls/Get.java) - Batches - [voice/v2/batches/Start](./v2/batches/Start.java) - [voice/v2/batches/Get](./v2/batches/Get.java) diff --git a/examples/snippets/src/main/java/voice/v2/calls/Get.java b/examples/snippets/src/main/java/voice/v2/calls/Get.java new file mode 100644 index 000000000..9607323d5 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/calls/Get.java @@ -0,0 +1,47 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.calls; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class Get { + + private static final Logger LOGGER = Logger.getLogger(Get.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the call to retrieve + String callId = "CALL_ID"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + CallsService callsService = client.voice().v2().calls(); + + LOGGER.info(String.format("Get call with ID '%s'", callId)); + + Call response = callsService.get(callId); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java index c10048499..c20dcd288 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java @@ -1,6 +1,7 @@ package com.sinch.sdk.domains.voice.api.v2; import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.domains.voice.models.v2.Call; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; @@ -33,4 +34,16 @@ public interface CallsService { StartCallResponse start( StartCallQueryParameters queryParameter, StartCallRequest startCallRequest) throws ApiException; + + /** + * Retrieve call details by call ID + * + *

Retrieve detailed information about a specific call using its unique identifier. + * + * @param callId The ID of the call. (required) + * @return Call + * @throws ApiException if fails to make API call + * @since 2.3 + */ + Call get(String callId) throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java index 61ee593fc..4b271f532 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java @@ -14,6 +14,7 @@ import com.sinch.sdk.core.http.URLParameterUtils; import com.sinch.sdk.core.http.URLPathUtils; import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.domains.voice.models.v2.Call; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; @@ -138,4 +139,68 @@ private HttpRequest startRequestBuilder( localVarContentTypes, localVarAuthNames); } + + @Override + public Call get(String callId) throws ApiException { + + LOGGER.finest("[get]" + " " + "callId: " + callId); + + HttpRequest httpRequest = getRequestBuilder(callId); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest getRequestBuilder(String callId) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling get"); + } + // verify the required parameter 'callId' is set + if (callId == null) { + throw new ApiException(400, "Missing the required parameter 'callId' when calling get"); + } + + String localVarPath = + "/v2/projects/{projectId}/calls/{callId}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "callId" + "\\}", URLPathUtils.encodePathSegment(callId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.GET, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } From 9b56b72b79921e4079be19183471397e606c469d Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 10:05:59 +0200 Subject: [PATCH 27/50] Add Voice v2 calls.list endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/CallsServiceTest.java | 145 +++++++++++ .../sdk/e2e/domains/voice/v2/CallsSteps.java | 46 ++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 1 + .../snippets/src/main/java/voice/README.md | 1 + .../src/main/java/voice/v2/calls/List.java | 48 ++++ .../domains/voice/api/v2/CallsService.java | 25 ++ .../api/v2/adapters/CallsServiceImpl.java | 187 ++++++++++++++ .../voice/models/v2/PaginationLinks.java | 114 +++++++++ .../voice/models/v2/PaginationLinksImpl.java | 201 +++++++++++++++ .../voice/models/v2/PaginationMeta.java | 64 +++++ .../voice/models/v2/PaginationMetaImpl.java | 119 +++++++++ .../request/ListCallsQueryParameters.java | 213 ++++++++++++++++ .../request/ListCallsQueryParametersImpl.java | 237 ++++++++++++++++++ .../v2/calls/response/CallsListResponse.java | 48 ++++ .../internal/CallsListResponseInternal.java | 86 +++++++ .../CallsListResponseInternalImpl.java | 151 +++++++++++ .../models/v2/PaginationLinksDtoTest.java | 35 +++ .../models/v2/PaginationMetaDtoTest.java | 25 ++ .../CallsListResponseInternalDtoTest.java | 69 +++++ .../domains/voice/v2/PaginationLinksDto.json | 7 + .../domains/voice/v2/PaginationMetaDto.json | 4 + .../CallsListResponseInternalDto.json | 67 +++++ .../CallsListResponseInternalLastPageDto.json | 30 +++ 24 files changed, 1925 insertions(+) create mode 100644 examples/snippets/src/main/java/voice/v2/calls/List.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationLinks.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationLinksImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationMeta.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationMetaImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/ListCallsQueryParameters.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/ListCallsQueryParametersImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/CallsListResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternal.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternalImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/PaginationLinksDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/PaginationMetaDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternalDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/PaginationLinksDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/PaginationMetaDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/calls/response/internal/CallsListResponseInternalDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/calls/response/internal/CallsListResponseInternalLastPageDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b132b44..e231ec1dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Support [Voice API v2](https://developers.sinch.com/docs/voice-2.0), exposed under `voice().v2()` - **[feature]** Voice V2 Calls API: `start` operation to create an outbound call - **[feature]** Voice V2 Calls API: `get` operation to retrieve a call +- **[feature]** Voice V2 Calls API: `list` operation to list and filter calls - **[feature]** Voice V2 Batches API: `start` operation to create a batch of outbound calls - **[feature]** Voice V2 Batches API: `get` operation to retrieve a batch summary - **[feature]** Voice V2 Batches API: `getDetails` operation to retrieve the per-session details of a batch @@ -29,6 +30,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet +- **[doc]** New `voice/v2/calls/List` snippet - **[doc]** New `voice/v2/batches/Start` snippet - **[doc]** New `voice/v2/batches/Get` snippet - **[doc]** New `voice/v2/batches/GetDetails` snippet diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java index 183ce189c..529dffd67 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java @@ -23,13 +23,22 @@ import com.sinch.sdk.domains.voice.api.v2.CallsService; import com.sinch.sdk.domains.voice.models.v2.Call; import com.sinch.sdk.domains.voice.models.v2.CallDtoTest; +import com.sinch.sdk.domains.voice.models.v2.CallReason; +import com.sinch.sdk.domains.voice.models.v2.CallResult; +import com.sinch.sdk.domains.voice.models.v2.CallType; +import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.CallsListResponse; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponseDtoTest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.internal.CallsListResponseInternalDtoTest; +import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -44,6 +53,7 @@ public class CallsServiceTest extends BaseTest { @Mock Map authManagers; static final String PROJECT_ID = "test_project_id"; + static final String CONFIGURED_SERVER_URL = "https://configured.server.com"; static final Collection AUTH_NAMES = Arrays.asList("BasicAuth", "SinchOAuth2"); static final Collection ACCEPTS = Arrays.asList(HttpContentType.APPLICATION_JSON, "application/problem+json"); @@ -59,6 +69,13 @@ public class CallsServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/CallDto.json") String jsonCallDto; + @GivenTextResource("/domains/voice/v2/calls/response/internal/CallsListResponseInternalDto.json") + String jsonCallsListResponseDto; + + @GivenTextResource( + "/domains/voice/v2/calls/response/internal/CallsListResponseInternalLastPageDto.json") + String jsonCallsListLastPageResponseDto; + @BeforeEach public void initMocks() { service = @@ -177,4 +194,132 @@ void getMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + + @Test + void list() throws ApiException { + + List commonParameters = + Arrays.asList( + new URLParameter( + "serviceId", "6e124178-c29d-46a5-943c-5c2ae544aade", URLParameter.form, true), + new URLParameter("from", "+15551234567", URLParameter.form, true), + new URLParameter("to", "+15551234568", URLParameter.form, true), + new URLParameter("callType", CallType.PHONE, URLParameter.form, true), + new URLParameter("startTime", "2025-02-01T14:00:00Z", URLParameter.form, true), + new URLParameter("endTime", "2025-03-01T14:00:00Z", URLParameter.form, true), + new URLParameter("callResult", CallResult.COMPLETED, URLParameter.form, true), + new URLParameter("callReason", CallReason.CALLEE_HANGUP, URLParameter.form, true), + new URLParameter("pageSize", 2, URLParameter.form, true)); + + List urlParameters = new ArrayList<>(commonParameters); + urlParameters.add(new URLParameter("page", 1, URLParameter.form, true)); + + mockListPage(urlParameters, jsonCallsListResponseDto); + mockNextListPage(jsonCallsListLastPageResponseDto); + + ListCallsQueryParameters queryParameters = + ListCallsQueryParameters.builder() + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setFrom("+15551234567") + .setTo("+15551234568") + .setCallType(CallType.PHONE) + .setStartTime(Instant.parse("2025-02-01T14:00:00Z")) + .setEndTime(Instant.parse("2025-03-01T14:00:00Z")) + .setCallResult(CallResult.COMPLETED) + .setCallReason(CallReason.CALLEE_HANGUP) + .setPageSize(2) + .setPage(1) + .build(); + + CallsListResponse response = service.list(queryParameters); + + TestHelpers.recursiveEquals( + new ArrayList<>(response.getContent()), + new ArrayList<>(CallsListResponseInternalDtoTest.expectedCallsListResponse.getCalls())); + Assertions.assertTrue(response.hasNextPage()); + + CallsListResponse nextResponse = response.nextPage(); + + TestHelpers.recursiveEquals( + new ArrayList<>(nextResponse.getContent()), + new ArrayList<>( + CallsListResponseInternalDtoTest.expectedCallsListLastPageResponse.getCalls())); + Assertions.assertFalse(nextResponse.hasNextPage()); + } + + @Test + void listIteratesOverAllPages() throws ApiException { + + mockListPage(Collections.emptyList(), jsonCallsListResponseDto); + mockNextListPage(jsonCallsListLastPageResponseDto); + + CallsListResponse response = service.list(); + + List calls = new ArrayList<>(); + response.iterator().forEachRemaining(calls::add); + + List expected = + new ArrayList<>(CallsListResponseInternalDtoTest.expectedCallsListResponse.getCalls()); + expected.addAll(CallsListResponseInternalDtoTest.expectedCallsListLastPageResponse.getCalls()); + TestHelpers.recursiveEquals(calls, expected); + } + + @Test + void listMissingProjectIdThrows() { + + CallsService serviceWithoutProjectId = + new CallsServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows(ApiException.class, () -> serviceWithoutProjectId.list()); + + Assertions.assertEquals(400, thrown.getCode()); + } + + // The "next" link served by the fixture targets https://voice.api.sinch.com: its path and query + // are expected to be sent to the configured server instead + private void mockNextListPage(String jsonResponse) { + when(serverConfiguration.getUrl()).thenReturn(CONFIGURED_SERVER_URL); + + HttpRequest httpRequest = + new HttpRequest( + CONFIGURED_SERVER_URL + + "/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=2&pageSize=2", + HttpMethod.GET, + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonResponse.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + } + + private void mockListPage(List urlParameters, String jsonResponse) { + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/calls", + HttpMethod.GET, + urlParameters, + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonResponse.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java index 4a7abc62b..df49da061 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java @@ -9,7 +9,9 @@ import com.sinch.sdk.domains.voice.models.v2.CallType; import com.sinch.sdk.domains.voice.models.v2.Money; import com.sinch.sdk.domains.voice.models.v2.OriginationType; +import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.CallsListResponse; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; import com.sinch.sdk.domains.voice.models.v2.destination.Phone; import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents; @@ -26,6 +28,7 @@ import java.time.Instant; import java.util.Arrays; import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Assertions; public class CallsSteps { @@ -33,6 +36,7 @@ public class CallsSteps { CallsService service; StartCallResponse startResponse; Call getResponse; + CallsListResponse listOnePageResponse; @Given("^the Voice-V2 service \"Calls\" is available$") public void serviceAvailable() { @@ -125,4 +129,46 @@ public void getResult() { TestHelpers.recursiveEquals(getResponse, expected); } + + @When("^I send a request to list calls$") + public void listOnePage() { + listOnePageResponse = service.list(ListCallsQueryParameters.builder().setPageSize(2).build()); + } + + @When("^I send a request to list all the calls$") + public void listAll() { + listOnePageResponse = service.list(ListCallsQueryParameters.builder().setPageSize(2).build()); + } + + @When("^I iterate manually over the calls pages$") + public void listAllByPage() { + listOnePageResponse = service.list(ListCallsQueryParameters.builder().setPageSize(2).build()); + } + + @Then("the response content contains \"{int}\" calls") + public void onePageResult(int expected) { + + Assertions.assertEquals(expected, listOnePageResponse.getContent().size()); + } + + @Then("the calls list contains \"{int}\" calls") + public void listAllResult(int expected) { + + AtomicInteger count = new AtomicInteger(); + listOnePageResponse.iterator().forEachRemaining(_unused -> count.getAndIncrement()); + + Assertions.assertEquals(expected, count.get()); + } + + @Then("the calls iteration result contains the data from \"{int}\" pages") + public void listAllByPageResult(int expected) { + + int count = listOnePageResponse.getContent().isEmpty() ? 0 : 1; + CallsListResponse currentPage = listOnePageResponse; + while (currentPage.hasNextPage()) { + count++; + currentPage = currentPage.nextPage(); + } + Assertions.assertEquals(expected, count); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 7500f812a..f29d1db66 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -32,6 +32,7 @@ "^\\[Start\\].*|^\\[Get\\] get a batch call summary$|^\\[GetDetails\\] get batch call" + " details$|^\\[Stop\\] stop batch processing$|^\\[Get\\] get a session$" + "|^\\[Get\\] get call details$" + + "|^\\[List\\] list (a page of|all the) calls.*$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 9954f84cb..000ff60a1 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -26,6 +26,7 @@ See main [README.md](../../../../README.md) for how to execute snippets - Calls - [voice/v2/calls/Start](./v2/calls/Start.java) - [voice/v2/calls/Get](./v2/calls/Get.java) + - [voice/v2/calls/List](./v2/calls/List.java) - Batches - [voice/v2/batches/Start](./v2/batches/Start.java) - [voice/v2/batches/Get](./v2/batches/Get.java) diff --git a/examples/snippets/src/main/java/voice/v2/calls/List.java b/examples/snippets/src/main/java/voice/v2/calls/List.java new file mode 100644 index 000000000..52fe241ff --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/calls/List.java @@ -0,0 +1,48 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.calls; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.calls.response.CallsListResponse; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class List { + + private static final Logger LOGGER = Logger.getLogger(List.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + CallsService callsService = client.voice().v2().calls(); + + LOGGER.info("Listing calls"); + + CallsListResponse response = callsService.list(); + + LOGGER.info("Response"); + + response + .iterator() + .forEachRemaining(f -> LOGGER.info(String.format("%s: %s", f.getCallId(), f))); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java index c20dcd288..317c99f8f 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java @@ -2,8 +2,10 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.CallsListResponse; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; /** Calls Service */ @@ -46,4 +48,27 @@ StartCallResponse start( * @since 2.3 */ Call get(String callId) throws ApiException; + + /** + * List calls made with Sinch Voice API + * + *

List and filter calls made with Sinch + * + * @return CallsListResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + CallsListResponse list() throws ApiException; + + /** + * List calls made with Sinch Voice API + * + *

List and filter calls made with Sinch + * + * @param queryParameter (optional) + * @return CallsListResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + CallsListResponse list(ListCallsQueryParameters queryParameter) throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java index 4b271f532..9fc2b708e 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java @@ -1,6 +1,7 @@ package com.sinch.sdk.domains.voice.api.v2.adapters; import com.fasterxml.jackson.core.type.TypeReference; +import com.sinch.sdk.core.databind.query_parameter.InstantToIso8601Serializer; import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.core.exceptions.ApiExceptionBuilder; import com.sinch.sdk.core.http.AuthManager; @@ -14,10 +15,19 @@ import com.sinch.sdk.core.http.URLParameterUtils; import com.sinch.sdk.core.http.URLPathUtils; import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.core.models.pagination.Page; +import com.sinch.sdk.core.models.pagination.PageNavigator; +import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; +import com.sinch.sdk.domains.voice.models.v2.calls.response.CallsListResponse; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; +import com.sinch.sdk.domains.voice.models.v2.calls.response.internal.CallsListResponseInternal; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -203,4 +213,181 @@ private HttpRequest getRequestBuilder(String callId) throws ApiException { localVarContentTypes, localVarAuthNames); } + + @Override + public CallsListResponse list() throws ApiException { + + return list((ListCallsQueryParameters) null); + } + + @Override + public CallsListResponse list(ListCallsQueryParameters queryParameter) throws ApiException { + + LOGGER.finest("[list]" + " " + "queryParameter: " + queryParameter); + + HttpRequest httpRequest = listRequestBuilder(queryParameter); + return _fetchListPage(httpRequest); + } + + private CallsListResponse _fetchListPage(HttpRequest httpRequest) throws ApiException { + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + + CallsListResponseInternal deserialized = + mapper.deserialize(response, new TypeReference() {}); + + PaginationLinks links = deserialized.getLinks(); + String nextLink = null != links ? links.getNext() : null; + final HttpRequest nextHttpRequest = + StringUtil.isEmpty(nextLink) ? null : nextPageRequestBuilder(nextLink); + + return new CallsListResponse( + () -> _fetchListPage(nextHttpRequest), + new Page<>(deserialized.getCalls(), new PageNavigator<>(nextHttpRequest))); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + // The API serves "next" as an absolute URL: its path and query are followed as they are, but + // against the configured server, so that credentials are never sent to a host taken from a + // response + private HttpRequest nextPageRequestBuilder(String nextLink) throws ApiException { + URI next; + URI server; + try { + next = new URI(nextLink); + server = new URI(this.serverConfiguration.getUrl()); + } catch (URISyntaxException e) { + throw new ApiException("Invalid next page link: " + nextLink, e); + } + + String fullUrl = + server.getScheme() + + "://" + + server.getRawAuthority() + + next.getRawPath() + + (null != next.getRawQuery() ? "?" + next.getRawQuery() : ""); + + return new HttpRequest( + fullUrl, + HttpMethod.GET, + null, + new HashMap<>(), + Arrays.asList("application/json", "application/problem+json"), + Arrays.asList(), + Arrays.asList("BasicAuth", "SinchOAuth2")); + } + + private HttpRequest listRequestBuilder(ListCallsQueryParameters queryParameter) + throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling list"); + } + + String localVarPath = + "/v2/projects/{projectId}/calls" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())); + + List localVarQueryParams = new ArrayList<>(); + if (null != queryParameter) { + + URLParameterUtils.addQueryParam( + queryParameter.getServiceId(), + "serviceId", + URLParameter.form, + null, + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getFrom(), "from", URLParameter.form, null, localVarQueryParams, true); + + URLParameterUtils.addQueryParam( + queryParameter.getTo(), "to", URLParameter.form, null, localVarQueryParams, true); + + URLParameterUtils.addQueryParam( + queryParameter.getCallType(), + "callType", + URLParameter.form, + null, + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getStartTime(), + "startTime", + URLParameter.form, + InstantToIso8601Serializer.getInstance(), + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getEndTime(), + "endTime", + URLParameter.form, + InstantToIso8601Serializer.getInstance(), + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getCallResult(), + "callResult", + URLParameter.form, + null, + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getCallReason(), + "callReason", + URLParameter.form, + null, + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getPageSize(), + "pageSize", + URLParameter.form, + null, + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getPage(), "page", URLParameter.form, null, localVarQueryParams, true); + } + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.GET, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationLinks.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationLinks.java new file mode 100644 index 000000000..49f6f3100 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationLinks.java @@ -0,0 +1,114 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** Pagination links for navigating through pages of results in a paginated list response. */ +@JsonDeserialize(builder = PaginationLinksImpl.Builder.class) +public interface PaginationLinks { + + /** + * Absolute URI of the first page. + * + *

Field is required + * + * @return first + */ + String getFirst(); + + /** + * Absolute URI of the last page. + * + *

Field is required + * + * @return last + */ + String getLast(); + + /** + * Absolute URI of the next page (omitted if this is the last page). + * + * @return next + */ + String getNext(); + + /** + * Absolute URI of the previous page (omitted if this is the first page). + * + * @return prev + */ + String getPrev(); + + /** + * Absolute URI of the current page. + * + *

Field is required + * + * @return self + */ + String getSelf(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new PaginationLinksImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param first see getter + * @return Current builder + * @see #getFirst + */ + Builder setFirst(String first); + + /** + * see getter + * + * @param last see getter + * @return Current builder + * @see #getLast + */ + Builder setLast(String last); + + /** + * see getter + * + * @param next see getter + * @return Current builder + * @see #getNext + */ + Builder setNext(String next); + + /** + * see getter + * + * @param prev see getter + * @return Current builder + * @see #getPrev + */ + Builder setPrev(String prev); + + /** + * see getter + * + * @param self see getter + * @return Current builder + * @see #getSelf + */ + Builder setSelf(String self); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + PaginationLinks build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationLinksImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationLinksImpl.java new file mode 100644 index 000000000..ed1c2a52e --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationLinksImpl.java @@ -0,0 +1,201 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + PaginationLinksImpl.JSON_PROPERTY_FIRST, + PaginationLinksImpl.JSON_PROPERTY_LAST, + PaginationLinksImpl.JSON_PROPERTY_NEXT, + PaginationLinksImpl.JSON_PROPERTY_PREV, + PaginationLinksImpl.JSON_PROPERTY_SELF +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class PaginationLinksImpl implements PaginationLinks { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_FIRST = "first"; + + private OptionalValue first; + + public static final String JSON_PROPERTY_LAST = "last"; + + private OptionalValue last; + + public static final String JSON_PROPERTY_NEXT = "next"; + + private OptionalValue next; + + public static final String JSON_PROPERTY_PREV = "prev"; + + private OptionalValue prev; + + public static final String JSON_PROPERTY_SELF = "self"; + + private OptionalValue self; + + public PaginationLinksImpl() {} + + protected PaginationLinksImpl( + OptionalValue first, + OptionalValue last, + OptionalValue next, + OptionalValue prev, + OptionalValue self) { + this.first = first; + this.last = last; + this.next = next; + this.prev = prev; + this.self = self; + } + + @JsonIgnore + public String getFirst() { + return first.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_FIRST) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue first() { + return first; + } + + @JsonIgnore + public String getLast() { + return last.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_LAST) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue last() { + return last; + } + + @JsonIgnore + public String getNext() { + return next.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_NEXT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue next() { + return next; + } + + @JsonIgnore + public String getPrev() { + return prev.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PREV) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue prev() { + return prev; + } + + @JsonIgnore + public String getSelf() { + return self.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SELF) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue self() { + return self; + } + + /** Return true if this PaginationLinks object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PaginationLinksImpl paginationLinks = (PaginationLinksImpl) o; + return Objects.equals(this.first, paginationLinks.first) + && Objects.equals(this.last, paginationLinks.last) + && Objects.equals(this.next, paginationLinks.next) + && Objects.equals(this.prev, paginationLinks.prev) + && Objects.equals(this.self, paginationLinks.self); + } + + @Override + public int hashCode() { + return Objects.hash(first, last, next, prev, self); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PaginationLinksImpl {\n"); + sb.append(" first: ").append(toIndentedString(first)).append("\n"); + sb.append(" last: ").append(toIndentedString(last)).append("\n"); + sb.append(" next: ").append(toIndentedString(next)).append("\n"); + sb.append(" prev: ").append(toIndentedString(prev)).append("\n"); + sb.append(" self: ").append(toIndentedString(self)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements PaginationLinks.Builder { + OptionalValue first = OptionalValue.empty(); + OptionalValue last = OptionalValue.empty(); + OptionalValue next = OptionalValue.empty(); + OptionalValue prev = OptionalValue.empty(); + OptionalValue self = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_FIRST, required = true) + public Builder setFirst(String first) { + this.first = OptionalValue.of(first); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_LAST, required = true) + public Builder setLast(String last) { + this.last = OptionalValue.of(last); + return this; + } + + @JsonProperty(JSON_PROPERTY_NEXT) + public Builder setNext(String next) { + this.next = OptionalValue.of(next); + return this; + } + + @JsonProperty(JSON_PROPERTY_PREV) + public Builder setPrev(String prev) { + this.prev = OptionalValue.of(prev); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_SELF, required = true) + public Builder setSelf(String self) { + this.self = OptionalValue.of(self); + return this; + } + + public PaginationLinks build() { + return new PaginationLinksImpl(first, last, next, prev, self); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationMeta.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationMeta.java new file mode 100644 index 000000000..d54f5f7cf --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationMeta.java @@ -0,0 +1,64 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** Metadata about the paginated list response. */ +@JsonDeserialize(builder = PaginationMetaImpl.Builder.class) +public interface PaginationMeta { + + /** + * Total number of items across all pages. + * + *

Field is required + * + * @return totalCount + */ + Integer getTotalCount(); + + /** + * Total number of pages. + * + *

Field is required + * + * @return pageCount + */ + Integer getPageCount(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new PaginationMetaImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param totalCount see getter + * @return Current builder + * @see #getTotalCount + */ + Builder setTotalCount(Integer totalCount); + + /** + * see getter + * + * @param pageCount see getter + * @return Current builder + * @see #getPageCount + */ + Builder setPageCount(Integer pageCount); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + PaginationMeta build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationMetaImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationMetaImpl.java new file mode 100644 index 000000000..9c25a86da --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/PaginationMetaImpl.java @@ -0,0 +1,119 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + PaginationMetaImpl.JSON_PROPERTY_TOTAL_COUNT, + PaginationMetaImpl.JSON_PROPERTY_PAGE_COUNT +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class PaginationMetaImpl implements PaginationMeta { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_TOTAL_COUNT = "totalCount"; + + private OptionalValue totalCount; + + public static final String JSON_PROPERTY_PAGE_COUNT = "pageCount"; + + private OptionalValue pageCount; + + public PaginationMetaImpl() {} + + protected PaginationMetaImpl( + OptionalValue totalCount, OptionalValue pageCount) { + this.totalCount = totalCount; + this.pageCount = pageCount; + } + + @JsonIgnore + public Integer getTotalCount() { + return totalCount.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TOTAL_COUNT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue totalCount() { + return totalCount; + } + + @JsonIgnore + public Integer getPageCount() { + return pageCount.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PAGE_COUNT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue pageCount() { + return pageCount; + } + + /** Return true if this PaginationMeta object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PaginationMetaImpl paginationMeta = (PaginationMetaImpl) o; + return Objects.equals(this.totalCount, paginationMeta.totalCount) + && Objects.equals(this.pageCount, paginationMeta.pageCount); + } + + @Override + public int hashCode() { + return Objects.hash(totalCount, pageCount); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class PaginationMetaImpl {\n"); + sb.append(" totalCount: ").append(toIndentedString(totalCount)).append("\n"); + sb.append(" pageCount: ").append(toIndentedString(pageCount)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements PaginationMeta.Builder { + OptionalValue totalCount = OptionalValue.empty(); + OptionalValue pageCount = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TOTAL_COUNT, required = true) + public Builder setTotalCount(Integer totalCount) { + this.totalCount = OptionalValue.of(totalCount); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_PAGE_COUNT, required = true) + public Builder setPageCount(Integer pageCount) { + this.pageCount = OptionalValue.of(pageCount); + return this; + } + + public PaginationMeta build() { + return new PaginationMetaImpl(totalCount, pageCount); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/ListCallsQueryParameters.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/ListCallsQueryParameters.java new file mode 100644 index 000000000..7f72232dc --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/ListCallsQueryParameters.java @@ -0,0 +1,213 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.request; + +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.CallReason; +import com.sinch.sdk.domains.voice.models.v2.CallResult; +import com.sinch.sdk.domains.voice.models.v2.CallType; +import java.time.Instant; + +/** + * Query parameters for listing calls + * + * @since 2.3 + */ +public interface ListCallsQueryParameters { + + /** + * The ID of the service. + * + * @return serviceId + */ + OptionalValue getServiceId(); + + /** + * Only include calls where from matches this origin. For inbound calls, this is the + * caller; for outbound calls, this is the calling party. + * + * @return from + */ + OptionalValue getFrom(); + + /** + * Only include calls where to matches this destination. For inbound calls, this is + * the called party; for outbound calls, this is the callee/recipient. + * + * @return to + */ + OptionalValue getTo(); + + /** + * Only include calls of the specified type. + * + *

If omitted, calls of all types are included. + * + * @return callType + */ + OptionalValue getCallType(); + + /** + * Only include calls that started at or after startTime. + * + * @return startTime + */ + OptionalValue getStartTime(); + + /** + * Only include calls that ended before endTime (exclusive). + * + * @return endTime + */ + OptionalValue getEndTime(); + + /** + * Filter results to only include calls whose callResult matches the specified value. + * + *

If omitted, calls with any result are included. + * + * @return callResult + */ + OptionalValue getCallResult(); + + /** + * Filter results to only include calls whose callReason matches the specified value. + * + *

If omitted, calls with any reason are included. + * + * @return callReason + */ + OptionalValue getCallReason(); + + /** + * Number of items to be returned on each page. minimum: 1 maximum: 100 + * + * @return pageSize + */ + OptionalValue getPageSize(); + + /** + * Page number (1-based) minimum: 1 + * + * @return page + */ + OptionalValue getPage(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new ListCallsQueryParametersImpl.Builder(); + } + + /** + * Getting builder from existing instance + * + * @param parameters Instance to copy the values from + * @return New Builder instance + */ + static Builder builder(ListCallsQueryParameters parameters) { + return new ListCallsQueryParametersImpl.Builder(parameters); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param serviceId see getter + * @return Current builder + * @see #getServiceId + */ + Builder setServiceId(String serviceId); + + /** + * see getter + * + * @param from see getter + * @return Current builder + * @see #getFrom + */ + Builder setFrom(String from); + + /** + * see getter + * + * @param to see getter + * @return Current builder + * @see #getTo + */ + Builder setTo(String to); + + /** + * see getter + * + * @param callType see getter + * @return Current builder + * @see #getCallType + */ + Builder setCallType(CallType callType); + + /** + * see getter + * + * @param startTime see getter + * @return Current builder + * @see #getStartTime + */ + Builder setStartTime(Instant startTime); + + /** + * see getter + * + * @param endTime see getter + * @return Current builder + * @see #getEndTime + */ + Builder setEndTime(Instant endTime); + + /** + * see getter + * + * @param callResult see getter + * @return Current builder + * @see #getCallResult + */ + Builder setCallResult(CallResult callResult); + + /** + * see getter + * + * @param callReason see getter + * @return Current builder + * @see #getCallReason + */ + Builder setCallReason(CallReason callReason); + + /** + * see getter + * + * @param pageSize see getter + * @return Current builder + * @see #getPageSize + */ + Builder setPageSize(Integer pageSize); + + /** + * see getter + * + * @param page see getter + * @return Current builder + * @see #getPage + */ + Builder setPage(Integer page); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + ListCallsQueryParameters build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/ListCallsQueryParametersImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/ListCallsQueryParametersImpl.java new file mode 100644 index 000000000..7694ce03f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/ListCallsQueryParametersImpl.java @@ -0,0 +1,237 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.request; + +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.CallReason; +import com.sinch.sdk.domains.voice.models.v2.CallResult; +import com.sinch.sdk.domains.voice.models.v2.CallType; +import java.time.Instant; +import java.util.Objects; + +public class ListCallsQueryParametersImpl implements ListCallsQueryParameters { + + private final OptionalValue serviceId; + private final OptionalValue from; + private final OptionalValue to; + private final OptionalValue callType; + private final OptionalValue startTime; + private final OptionalValue endTime; + private final OptionalValue callResult; + private final OptionalValue callReason; + private final OptionalValue pageSize; + private final OptionalValue page; + + private ListCallsQueryParametersImpl( + OptionalValue serviceId, + OptionalValue from, + OptionalValue to, + OptionalValue callType, + OptionalValue startTime, + OptionalValue endTime, + OptionalValue callResult, + OptionalValue callReason, + OptionalValue pageSize, + OptionalValue page) { + this.serviceId = serviceId; + this.from = from; + this.to = to; + this.callType = callType; + this.startTime = startTime; + this.endTime = endTime; + this.callResult = callResult; + this.callReason = callReason; + this.pageSize = pageSize; + this.page = page; + } + + public OptionalValue getServiceId() { + return serviceId; + } + + public OptionalValue getFrom() { + return from; + } + + public OptionalValue getTo() { + return to; + } + + public OptionalValue getCallType() { + return callType; + } + + public OptionalValue getStartTime() { + return startTime; + } + + public OptionalValue getEndTime() { + return endTime; + } + + public OptionalValue getCallResult() { + return callResult; + } + + public OptionalValue getCallReason() { + return callReason; + } + + public OptionalValue getPageSize() { + return pageSize; + } + + public OptionalValue getPage() { + return page; + } + + /** Return true if this ListCallsQueryParameters object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListCallsQueryParametersImpl parameters = (ListCallsQueryParametersImpl) o; + return Objects.equals(this.serviceId, parameters.serviceId) + && Objects.equals(this.from, parameters.from) + && Objects.equals(this.to, parameters.to) + && Objects.equals(this.callType, parameters.callType) + && Objects.equals(this.startTime, parameters.startTime) + && Objects.equals(this.endTime, parameters.endTime) + && Objects.equals(this.callResult, parameters.callResult) + && Objects.equals(this.callReason, parameters.callReason) + && Objects.equals(this.pageSize, parameters.pageSize) + && Objects.equals(this.page, parameters.page); + } + + @Override + public int hashCode() { + return Objects.hash( + serviceId, from, to, callType, startTime, endTime, callResult, callReason, pageSize, page); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListCallsQueryParametersImpl {\n"); + sb.append(" serviceId: ").append(toIndentedString(serviceId)).append("\n"); + sb.append(" from: ").append(toIndentedString(from)).append("\n"); + sb.append(" to: ").append(toIndentedString(to)).append("\n"); + sb.append(" callType: ").append(toIndentedString(callType)).append("\n"); + sb.append(" startTime: ").append(toIndentedString(startTime)).append("\n"); + sb.append(" endTime: ").append(toIndentedString(endTime)).append("\n"); + sb.append(" callResult: ").append(toIndentedString(callResult)).append("\n"); + sb.append(" callReason: ").append(toIndentedString(callReason)).append("\n"); + sb.append(" pageSize: ").append(toIndentedString(pageSize)).append("\n"); + sb.append(" page: ").append(toIndentedString(page)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + static class Builder implements ListCallsQueryParameters.Builder { + OptionalValue serviceId = OptionalValue.empty(); + OptionalValue from = OptionalValue.empty(); + OptionalValue to = OptionalValue.empty(); + OptionalValue callType = OptionalValue.empty(); + OptionalValue startTime = OptionalValue.empty(); + OptionalValue endTime = OptionalValue.empty(); + OptionalValue callResult = OptionalValue.empty(); + OptionalValue callReason = OptionalValue.empty(); + OptionalValue pageSize = OptionalValue.empty(); + OptionalValue page = OptionalValue.empty(); + + protected Builder() {} + + protected Builder(ListCallsQueryParameters _parameters) { + if (null == _parameters) { + return; + } + ListCallsQueryParametersImpl parameters = (ListCallsQueryParametersImpl) _parameters; + this.serviceId = parameters.getServiceId(); + this.from = parameters.getFrom(); + this.to = parameters.getTo(); + this.callType = parameters.getCallType(); + this.startTime = parameters.getStartTime(); + this.endTime = parameters.getEndTime(); + this.callResult = parameters.getCallResult(); + this.callReason = parameters.getCallReason(); + this.pageSize = parameters.getPageSize(); + this.page = parameters.getPage(); + } + + public Builder setServiceId(String serviceId) { + this.serviceId = OptionalValue.of(serviceId); + return this; + } + + public Builder setFrom(String from) { + this.from = OptionalValue.of(from); + return this; + } + + public Builder setTo(String to) { + this.to = OptionalValue.of(to); + return this; + } + + public Builder setCallType(CallType callType) { + this.callType = OptionalValue.of(callType); + return this; + } + + public Builder setStartTime(Instant startTime) { + this.startTime = OptionalValue.of(startTime); + return this; + } + + public Builder setEndTime(Instant endTime) { + this.endTime = OptionalValue.of(endTime); + return this; + } + + public Builder setCallResult(CallResult callResult) { + this.callResult = OptionalValue.of(callResult); + return this; + } + + public Builder setCallReason(CallReason callReason) { + this.callReason = OptionalValue.of(callReason); + return this; + } + + public Builder setPageSize(Integer pageSize) { + this.pageSize = OptionalValue.of(pageSize); + return this; + } + + public Builder setPage(Integer page) { + this.page = OptionalValue.of(page); + return this; + } + + public ListCallsQueryParameters build() { + return new ListCallsQueryParametersImpl( + serviceId, + from, + to, + callType, + startTime, + endTime, + callResult, + callReason, + pageSize, + page); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/CallsListResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/CallsListResponse.java new file mode 100644 index 000000000..f7f69e6a0 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/CallsListResponse.java @@ -0,0 +1,48 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.response; + +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.models.pagination.ListResponse; +import com.sinch.sdk.core.models.pagination.Page; +import com.sinch.sdk.domains.voice.models.v2.Call; +import java.util.Collection; +import java.util.Collections; +import java.util.NoSuchElementException; +import java.util.function.Supplier; + +/** Auto paginated response for list of Call */ +public class CallsListResponse extends ListResponse { + + private final Page page; + private final Supplier supplier; + + public CallsListResponse(Supplier supplier, Page page) { + this.supplier = supplier; + this.page = page; + } + + @Override + public boolean hasNextPage() { + if (null == page.getNextPageToken() || null == getContent() || getContent().isEmpty()) { + return false; + } + return true; + } + + @Override + public CallsListResponse nextPage() { + if (!hasNextPage()) { + throw new NoSuchElementException("Reached the last page of the API response"); + } + return supplier.get(); + } + + @Override + public Collection getContent() { + return page == null ? Collections.emptyList() : page.getEntities(); + } + + @Override + public String toString() { + return "CallsListResponse {" + "page=" + page + '}'; + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternal.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternal.java new file mode 100644 index 000000000..1ea431d4f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternal.java @@ -0,0 +1,86 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.response.internal; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import com.sinch.sdk.domains.voice.models.v2.PaginationMeta; +import java.util.List; + +/** List of calls. */ +@JsonDeserialize(builder = CallsListResponseInternalImpl.Builder.class) +public interface CallsListResponseInternal { + + /** + * Array of call resources + * + *

Field is required + * + * @return calls + */ + List getCalls(); + + /** + * Get links + * + *

Field is required + * + * @return links + */ + PaginationLinks getLinks(); + + /** + * Get meta + * + *

Field is required + * + * @return meta + */ + PaginationMeta getMeta(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new CallsListResponseInternalImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param calls see getter + * @return Current builder + * @see #getCalls + */ + Builder setCalls(List calls); + + /** + * see getter + * + * @param links see getter + * @return Current builder + * @see #getLinks + */ + Builder setLinks(PaginationLinks links); + + /** + * see getter + * + * @param meta see getter + * @return Current builder + * @see #getMeta + */ + Builder setMeta(PaginationMeta meta); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + CallsListResponseInternal build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternalImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternalImpl.java new file mode 100644 index 000000000..6ae46860b --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternalImpl.java @@ -0,0 +1,151 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.response.internal; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import com.sinch.sdk.domains.voice.models.v2.PaginationMeta; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + CallsListResponseInternalImpl.JSON_PROPERTY_CALLS, + CallsListResponseInternalImpl.JSON_PROPERTY_LINKS, + CallsListResponseInternalImpl.JSON_PROPERTY_META +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class CallsListResponseInternalImpl implements CallsListResponseInternal { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_CALLS = "calls"; + + private OptionalValue> calls; + + public static final String JSON_PROPERTY_LINKS = "links"; + + private OptionalValue links; + + public static final String JSON_PROPERTY_META = "meta"; + + private OptionalValue meta; + + public CallsListResponseInternalImpl() {} + + protected CallsListResponseInternalImpl( + OptionalValue> calls, + OptionalValue links, + OptionalValue meta) { + this.calls = calls; + this.links = links; + this.meta = meta; + } + + @JsonIgnore + public List getCalls() { + return calls.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALLS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> calls() { + return calls; + } + + @JsonIgnore + public PaginationLinks getLinks() { + return links.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_LINKS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue links() { + return links; + } + + @JsonIgnore + public PaginationMeta getMeta() { + return meta.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_META) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue meta() { + return meta; + } + + /** Return true if this CallsListResponseInternal object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CallsListResponseInternalImpl callsListResponseInternal = (CallsListResponseInternalImpl) o; + return Objects.equals(this.calls, callsListResponseInternal.calls) + && Objects.equals(this.links, callsListResponseInternal.links) + && Objects.equals(this.meta, callsListResponseInternal.meta); + } + + @Override + public int hashCode() { + return Objects.hash(calls, links, meta); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CallsListResponseInternalImpl {\n"); + sb.append(" calls: ").append(toIndentedString(calls)).append("\n"); + sb.append(" links: ").append(toIndentedString(links)).append("\n"); + sb.append(" meta: ").append(toIndentedString(meta)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements CallsListResponseInternal.Builder { + OptionalValue> calls = OptionalValue.empty(); + OptionalValue links = OptionalValue.empty(); + OptionalValue meta = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_CALLS, required = true) + public Builder setCalls(List calls) { + this.calls = OptionalValue.of(calls); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_LINKS, required = true) + public Builder setLinks(PaginationLinks links) { + this.links = OptionalValue.of(links); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_META, required = true) + public Builder setMeta(PaginationMeta meta) { + this.meta = OptionalValue.of(meta); + return this; + } + + public CallsListResponseInternal build() { + return new CallsListResponseInternalImpl(calls, links, meta); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/PaginationLinksDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/PaginationLinksDtoTest.java new file mode 100644 index 000000000..5a830c985 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/PaginationLinksDtoTest.java @@ -0,0 +1,35 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class PaginationLinksDtoTest extends BaseTest { + + static final String BASE_URL = + "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls"; + + public static final PaginationLinks expectedPaginationLinks = + PaginationLinks.builder() + .setFirst(BASE_URL + "?page=1&pageSize=2") + .setLast(BASE_URL + "?page=2&pageSize=2") + .setNext(BASE_URL + "?page=3&pageSize=2") + .setPrev(BASE_URL + "?page=1&pageSize=2") + .setSelf(BASE_URL + "?page=2&pageSize=2") + .build(); + + @GivenTextResource("/domains/voice/v2/PaginationLinksDto.json") + String jsonPaginationLinks; + + @Test + void deserialize() throws JsonProcessingException { + PaginationLinks deserialized = + objectMapper.readValue(jsonPaginationLinks, PaginationLinks.class); + + TestHelpers.recursiveEquals(deserialized, expectedPaginationLinks); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/PaginationMetaDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/PaginationMetaDtoTest.java new file mode 100644 index 000000000..aa39393f8 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/PaginationMetaDtoTest.java @@ -0,0 +1,25 @@ +package com.sinch.sdk.domains.voice.models.v2; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class PaginationMetaDtoTest extends BaseTest { + + public static final PaginationMeta expectedPaginationMeta = + PaginationMeta.builder().setTotalCount(3).setPageCount(2).build(); + + @GivenTextResource("/domains/voice/v2/PaginationMetaDto.json") + String jsonPaginationMeta; + + @Test + void deserialize() throws JsonProcessingException { + PaginationMeta deserialized = objectMapper.readValue(jsonPaginationMeta, PaginationMeta.class); + + TestHelpers.recursiveEquals(deserialized, expectedPaginationMeta); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternalDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternalDtoTest.java new file mode 100644 index 000000000..981f1df3d --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/response/internal/CallsListResponseInternalDtoTest.java @@ -0,0 +1,69 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.response.internal; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.CallDtoTest; +import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import com.sinch.sdk.domains.voice.models.v2.PaginationMetaDtoTest; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class CallsListResponseInternalDtoTest extends BaseTest { + + static final String BASE_URL = + "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls"; + + public static final CallsListResponseInternal expectedCallsListResponse = + CallsListResponseInternal.builder() + .setCalls(Arrays.asList(CallDtoTest.expectedCall, CallDtoTest.expectedCallMinimal)) + .setLinks( + PaginationLinks.builder() + .setFirst(BASE_URL + "?page=1&pageSize=2") + .setLast(BASE_URL + "?page=2&pageSize=2") + .setNext(BASE_URL + "?page=2&pageSize=2") + .setSelf(BASE_URL + "?page=1&pageSize=2") + .build()) + .setMeta(PaginationMetaDtoTest.expectedPaginationMeta) + .build(); + + public static final CallsListResponseInternal expectedCallsListLastPageResponse = + CallsListResponseInternal.builder() + .setCalls(Collections.singletonList(CallDtoTest.expectedCallMinimal)) + .setLinks( + PaginationLinks.builder() + .setFirst(BASE_URL + "?page=1&pageSize=2") + .setLast(BASE_URL + "?page=2&pageSize=2") + .setPrev(BASE_URL + "?page=1&pageSize=2") + .setSelf(BASE_URL + "?page=2&pageSize=2") + .build()) + .setMeta(PaginationMetaDtoTest.expectedPaginationMeta) + .build(); + + @GivenTextResource("/domains/voice/v2/calls/response/internal/CallsListResponseInternalDto.json") + String jsonCallsListResponse; + + @GivenTextResource( + "/domains/voice/v2/calls/response/internal/CallsListResponseInternalLastPageDto.json") + String jsonCallsListLastPageResponse; + + @Test + void deserialize() throws JsonProcessingException { + CallsListResponseInternal deserialized = + objectMapper.readValue(jsonCallsListResponse, CallsListResponseInternal.class); + + TestHelpers.recursiveEquals(deserialized, expectedCallsListResponse); + } + + @Test + void deserializeLastPage() throws JsonProcessingException { + CallsListResponseInternal deserialized = + objectMapper.readValue(jsonCallsListLastPageResponse, CallsListResponseInternal.class); + + TestHelpers.recursiveEquals(deserialized, expectedCallsListLastPageResponse); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/PaginationLinksDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/PaginationLinksDto.json new file mode 100644 index 000000000..057f11fd4 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/PaginationLinksDto.json @@ -0,0 +1,7 @@ +{ + "first": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=1&pageSize=2", + "last": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=2&pageSize=2", + "next": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=3&pageSize=2", + "prev": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=1&pageSize=2", + "self": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=2&pageSize=2" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/PaginationMetaDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/PaginationMetaDto.json new file mode 100644 index 000000000..d6679aa5b --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/PaginationMetaDto.json @@ -0,0 +1,4 @@ +{ + "totalCount": 3, + "pageCount": 2 +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/internal/CallsListResponseInternalDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/internal/CallsListResponseInternalDto.json new file mode 100644 index 000000000..2e1fc50e9 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/internal/CallsListResponseInternalDto.json @@ -0,0 +1,67 @@ +{ + "calls": [ + { + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "callName": "origin", + "bridgeName": "my-bridge", + "batchId": "01BX5ZZKBKACTAV9WEVGEMMVRC", + "from": { + "type": "SIP", + "sip": { + "endpoint": "sip:alice@sip.example.com", + "displayName": "Alice" + } + }, + "to": { + "type": "PHONE", + "phone": { + "number": "+15551234567" + } + }, + "startTime": "2025-02-10T09:00:00Z", + "updateTime": "2025-02-10T09:00:47Z", + "callType": "PHONE", + "direction": "INBOUND", + "answerTime": "2025-02-10T09:00:05Z", + "endTime": "2025-02-10T09:00:47Z", + "callDurationSeconds": 42, + "callResult": "COMPLETED", + "callReason": "CALLEE_HANGUP", + "originationType": "SIP", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" + }, + { + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "startTime": "2025-02-10T09:00:00Z", + "callType": "PHONE", + "direction": "OUTBOUND", + "callResult": "QUEUED", + "originationType": "SERVER", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" + } + ], + "links": { + "first": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=1&pageSize=2", + "last": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=2&pageSize=2", + "next": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=2&pageSize=2", + "self": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=1&pageSize=2" + }, + "meta": { + "totalCount": 3, + "pageCount": 2 + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/internal/CallsListResponseInternalLastPageDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/internal/CallsListResponseInternalLastPageDto.json new file mode 100644 index 000000000..e618a532c --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/calls/response/internal/CallsListResponseInternalLastPageDto.json @@ -0,0 +1,30 @@ +{ + "calls": [ + { + "callId": "01ARZ3NDEKTSV4RRFFQ69G5FAA", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "sessionId": "01BX5ZZKBKACTAV9WEVGEMMVRB", + "startTime": "2025-02-10T09:00:00Z", + "callType": "PHONE", + "direction": "OUTBOUND", + "callResult": "QUEUED", + "originationType": "SERVER", + "callRate": { + "currencyCode": "USD", + "amount": "0.0123" + }, + "callResourceUrl": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls/01ARZ3NDEKTSV4RRFFQ69G5FAA" + } + ], + "links": { + "first": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=1&pageSize=2", + "last": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=2&pageSize=2", + "prev": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=1&pageSize=2", + "self": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/calls?page=2&pageSize=2" + }, + "meta": { + "totalCount": 3, + "pageCount": 2 + } +} From 9cee535ce3a5a0d8d675c5fb4718614b3beb9d6b Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 10:52:32 +0200 Subject: [PATCH 28/50] Add Voice v2 calls.interactByCallId endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/CallsServiceTest.java | 80 ++++++++++++ .../sdk/e2e/domains/voice/v2/CallsSteps.java | 32 +++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 1 + .../snippets/src/main/java/voice/README.md | 1 + .../java/voice/v2/calls/InteractByCallId.java | 54 ++++++++ .../domains/voice/api/v2/CallsService.java | 14 +++ .../api/v2/adapters/CallsServiceImpl.java | 82 +++++++++++++ .../v2/calls/request/CallPatchRequest.java | 81 ++++++++++++ .../calls/request/CallPatchRequestImpl.java | 115 ++++++++++++++++++ .../request/CallPatchRequestDtoTest.java | 35 ++++++ .../v2/calls/request/CallPatchRequestDto.json | 36 ++++++ 12 files changed, 533 insertions(+) create mode 100644 examples/snippets/src/main/java/voice/v2/calls/InteractByCallId.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequestImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequestDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/calls/request/CallPatchRequestDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e231ec1dc..bf0ac695c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Calls API: `start` operation to create an outbound call - **[feature]** Voice V2 Calls API: `get` operation to retrieve a call - **[feature]** Voice V2 Calls API: `list` operation to list and filter calls +- **[feature]** Voice V2 Calls API: `interactByCallId` operation to send SVAML commands to an ongoing call - **[feature]** Voice V2 Batches API: `start` operation to create a batch of outbound calls - **[feature]** Voice V2 Batches API: `get` operation to retrieve a batch summary - **[feature]** Voice V2 Batches API: `getDetails` operation to retrieve the per-session details of a batch @@ -31,6 +32,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet - **[doc]** New `voice/v2/calls/List` snippet +- **[doc]** New `voice/v2/calls/InteractByCallId` snippet - **[doc]** New `voice/v2/batches/Start` snippet - **[doc]** New `voice/v2/batches/Get` snippet - **[doc]** New `voice/v2/batches/GetDetails` snippet diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java index 529dffd67..36d534af8 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java @@ -2,6 +2,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.adelean.inject.resources.junit.jupiter.GivenTextResource; @@ -26,6 +27,7 @@ import com.sinch.sdk.domains.voice.models.v2.CallReason; import com.sinch.sdk.domains.voice.models.v2.CallResult; import com.sinch.sdk.domains.voice.models.v2.CallType; +import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequestDtoTest; import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequestDtoTest; @@ -63,6 +65,9 @@ public class CallsServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/calls/request/StartCallRequestDto.json") String jsonStartCallRequestDto; + @GivenTextResource("/domains/voice/v2/calls/request/CallPatchRequestDto.json") + String jsonCallPatchRequestDto; + @GivenTextResource("/domains/voice/v2/calls/response/StartCallResponseDto.json") String jsonStartCallResponseDto; @@ -322,4 +327,79 @@ private void mockListPage(List urlParameters, String jsonResponse) argThat(new HttpRequestMatcher(httpRequest)))) .thenReturn(httpResponse); } + + @Test + void interactByCallId() throws ApiException { + + String callId = "01ARZ3NDEKTSV4RRFFQ69G5FAA"; + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/calls/" + + URLPathUtils.encodePathSegment(callId), + HttpMethod.PATCH, + Collections.emptyList(), + jsonCallPatchRequestDto, + Collections.singletonMap( + "Idempotency-Key", + CallPatchRequestDtoTest.expectedCallPatchRequest.getIdempotencyKey()), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = new HttpResponse(202, null, Collections.emptyMap(), null); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + service.interactByCallId(callId, CallPatchRequestDtoTest.expectedCallPatchRequest); + + verify(httpClient) + .invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest))); + } + + @Test + void interactByCallIdMissingCallIdThrows() { + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> service.interactByCallId(null, CallPatchRequestDtoTest.expectedCallPatchRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void interactByCallIdMissingRequestThrows() { + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, () -> service.interactByCallId("01ARZ3NDEKTSV4RRFFQ69G5FAA", null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void interactByCallIdMissingProjectIdThrows() { + + CallsService serviceWithoutProjectId = + new CallsServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> + serviceWithoutProjectId.interactByCallId( + "01ARZ3NDEKTSV4RRFFQ69G5FAA", + CallPatchRequestDtoTest.expectedCallPatchRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java index df49da061..b97ad6560 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java @@ -9,6 +9,7 @@ import com.sinch.sdk.domains.voice.models.v2.CallType; import com.sinch.sdk.domains.voice.models.v2.Money; import com.sinch.sdk.domains.voice.models.v2.OriginationType; +import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest; import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; import com.sinch.sdk.domains.voice.models.v2.calls.response.CallsListResponse; @@ -37,6 +38,7 @@ public class CallsSteps { StartCallResponse startResponse; Call getResponse; CallsListResponse listOnePageResponse; + boolean interactionAccepted; @Given("^the Voice-V2 service \"Calls\" is available$") public void serviceAvailable() { @@ -171,4 +173,34 @@ public void listAllByPageResult(int expected) { } Assertions.assertEquals(expected, count); } + + @When("^I send a request to interact with an ongoing call by call ID$") + public void interactByCallId() { + MessagesCommand messages = + MessagesCommand.builder() + .setMessages( + Collections.singletonList( + SayMessage.builder() + .setText("Hello, your call is now connected.") + .setVoiceName("Emma") + .build())) + .setEvents( + MessageEvents.builder() + .setOnFinish(Collections.singletonList(HangupCommand.builder().build())) + .build()) + .build(); + + service.interactByCallId( + "01HZXK8FQNPMR8VD3JW9YF2C5A", + CallPatchRequest.builder() + .setCommands(Collections.singletonList(messages)) + .setIdempotencyKey("5d8f2a6c-1e4b-4c9d-8a3f-7b6e0c2d9f14") + .build()); + interactionAccepted = true; + } + + @Then("^the response confirms the interaction request was accepted$") + public void interactResult() { + Assertions.assertTrue(interactionAccepted); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index f29d1db66..02e7be382 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -33,6 +33,7 @@ + " details$|^\\[Stop\\] stop batch processing$|^\\[Get\\] get a session$" + "|^\\[Get\\] get call details$" + "|^\\[List\\] list (a page of|all the) calls.*$" + + "|^\\[InteractByCallId\\] interact with an ongoing call by call ID$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 000ff60a1..90d84530b 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -27,6 +27,7 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/calls/Start](./v2/calls/Start.java) - [voice/v2/calls/Get](./v2/calls/Get.java) - [voice/v2/calls/List](./v2/calls/List.java) + - [voice/v2/calls/InteractByCallId](./v2/calls/InteractByCallId.java) - Batches - [voice/v2/batches/Start](./v2/batches/Start.java) - [voice/v2/batches/Get](./v2/batches/Get.java) diff --git a/examples/snippets/src/main/java/voice/v2/calls/InteractByCallId.java b/examples/snippets/src/main/java/voice/v2/calls/InteractByCallId.java new file mode 100644 index 000000000..14716beee --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/calls/InteractByCallId.java @@ -0,0 +1,54 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.calls; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; +import com.sinch.sdk.models.Configuration; +import java.util.Collections; +import java.util.logging.Logger; +import utils.Settings; + +public class InteractByCallId { + + private static final Logger LOGGER = Logger.getLogger(InteractByCallId.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the ongoing call to interact with + String callId = "CALL_ID"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + CallsService callsService = client.voice().v2().calls(); + + CallPatchRequest request = + CallPatchRequest.builder() + .setCommands(Collections.singletonList(HangupCommand.HANGUP_COMMAND)) + .build(); + + LOGGER.info(String.format("Interact with call with ID '%s'", callId)); + + callsService.interactByCallId(callId, request); + + LOGGER.info("Commands accepted for processing"); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java index 317c99f8f..57f031fe6 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java @@ -2,6 +2,7 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.domains.voice.models.v2.Call; +import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest; import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; @@ -71,4 +72,17 @@ StartCallResponse start( * @since 2.3 */ CallsListResponse list(ListCallsQueryParameters queryParameter) throws ApiException; + + /** + * Patch an ongoing call by call ID + * + *

Interact with an ongoing call by submitting a set of SVAML commands. Use this to force + * disconnect, play messages, bridge with another call, or perform other call control actions. + * + * @param callId The ID of the call. (required) + * @param callPatchRequest The SVAML commands to apply to the call (required) + * @throws ApiException if fails to make API call + * @since 2.3 + */ + void interactByCallId(String callId, CallPatchRequest callPatchRequest) throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java index 9fc2b708e..fc6c9a32d 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java @@ -20,6 +20,7 @@ import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.domains.voice.models.v2.Call; import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest; import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallRequest; @@ -390,4 +391,85 @@ private HttpRequest listRequestBuilder(ListCallsQueryParameters queryParameter) localVarContentTypes, localVarAuthNames); } + + @Override + public void interactByCallId(String callId, CallPatchRequest callPatchRequest) + throws ApiException { + + LOGGER.finest( + "[interactByCallId]" + + " " + + "callId: " + + callId + + ", " + + "callPatchRequest: " + + callPatchRequest); + + HttpRequest httpRequest = interactByCallIdRequestBuilder(callId, callPatchRequest); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return; + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest interactByCallIdRequestBuilder( + String callId, CallPatchRequest callPatchRequest) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling interactByCallId"); + } + // verify the required parameter 'callId' is set + if (callId == null) { + throw new ApiException( + 400, "Missing the required parameter 'callId' when calling interactByCallId"); + } + // verify the required parameter 'callPatchRequest' is set + if (callPatchRequest == null) { + throw new ApiException( + 400, "Missing the required parameter 'callPatchRequest' when calling interactByCallId"); + } + + String localVarPath = + "/v2/projects/{projectId}/calls/{callId}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "callId" + "\\}", URLPathUtils.encodePathSegment(callId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + if (callPatchRequest.getIdempotencyKey() != null) + localVarHeaderParams.put("Idempotency-Key", callPatchRequest.getIdempotencyKey()); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList("application/json"); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = mapper.serialize(localVarContentTypes, callPatchRequest); + + return new HttpRequest( + localVarPath, + HttpMethod.PATCH, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java new file mode 100644 index 000000000..c3d79c3c3 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java @@ -0,0 +1,81 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand; +import java.util.List; + +/** Request body for patching an ongoing call with SVAML commands. */ +@JsonDeserialize(builder = CallPatchRequestImpl.Builder.class) +public interface CallPatchRequest { + + /** + * An ordered list of SVAML v2 (Sinch Voice Application Markup Language) commands that describe a + * call flow. Commands are executed sequentially in the order they are defined. + * + *

Blocking vs. non-blocking: Some commands block execution until they complete (pause + * , webhook, menu, gotoMenu), while others return + * immediately and run in parallel (dial, messages, amd, + * answer, hangup, startRecording, stopRecording + * , bridgeCall, stopMessages). Each command's description + * specifies its behavior. + * + *

Nesting scope: Commands that appear inside event handlers (e.g., dial.events.onAnswer + * , messages.events.onFinish) form independent sequences and execute in their + * own context — they are not continuations of the parent sequence. + * + *

Field is required + * + * @return commands + */ + List getCommands(); + + /** + * Client-generated idempotency key to safely retry requests. The server uses this key to + * recognize retries of the same request. If a request with the same key is received within 10 + * minutes, the server returns the cached response from the original request. Using a random UUID + * (v4) is strongly recommended. + * + *

Sent as the Idempotency-Key header. + * + * @return idempotencyKey + */ + String getIdempotencyKey(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new CallPatchRequestImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param commands see getter + * @return Current builder + * @see #getCommands + */ + Builder setCommands(List commands); + + /** + * see getter + * + * @param idempotencyKey see getter + * @return Current builder + * @see #getIdempotencyKey + */ + Builder setIdempotencyKey(String idempotencyKey); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + CallPatchRequest build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequestImpl.java new file mode 100644 index 000000000..26eebdff0 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequestImpl.java @@ -0,0 +1,115 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.request; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({CallPatchRequestImpl.JSON_PROPERTY_COMMANDS}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class CallPatchRequestImpl implements CallPatchRequest { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMANDS = "commands"; + + private OptionalValue> commands; + + private OptionalValue idempotencyKey; + + public CallPatchRequestImpl() {} + + protected CallPatchRequestImpl( + OptionalValue> commands, OptionalValue idempotencyKey) { + this.commands = commands; + this.idempotencyKey = idempotencyKey; + } + + @JsonIgnore + public List getCommands() { + return commands.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMANDS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> commands() { + return commands; + } + + @JsonIgnore + public String getIdempotencyKey() { + return idempotencyKey.orElse(null); + } + + @JsonIgnore + public OptionalValue idempotencyKey() { + return idempotencyKey; + } + + /** Return true if this CallPatchRequest object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CallPatchRequestImpl callPatchRequest = (CallPatchRequestImpl) o; + return Objects.equals(this.commands, callPatchRequest.commands) + && Objects.equals(this.idempotencyKey, callPatchRequest.idempotencyKey); + } + + @Override + public int hashCode() { + return Objects.hash(commands, idempotencyKey); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CallPatchRequestImpl {\n"); + sb.append(" commands: ").append(toIndentedString(commands)).append("\n"); + sb.append(" idempotencyKey: ").append(toIndentedString(idempotencyKey)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements CallPatchRequest.Builder { + OptionalValue> commands = OptionalValue.empty(); + OptionalValue idempotencyKey = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMANDS, required = true) + public Builder setCommands(List commands) { + this.commands = OptionalValue.of(commands); + return this; + } + + @JsonIgnore + public Builder setIdempotencyKey(String idempotencyKey) { + this.idempotencyKey = OptionalValue.of(idempotencyKey); + return this; + } + + public CallPatchRequest build() { + return new CallPatchRequestImpl(commands, idempotencyKey); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequestDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequestDtoTest.java new file mode 100644 index 000000000..dc6be0fe9 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequestDtoTest.java @@ -0,0 +1,35 @@ +package com.sinch.sdk.domains.voice.models.v2.calls.request; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommandDtoTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommandDtoTest; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class CallPatchRequestDtoTest extends BaseTest { + + public static final CallPatchRequest expectedCallPatchRequest = + CallPatchRequest.builder() + .setCommands( + Arrays.asList( + MessagesCommandDtoTest.expectedMessagesCommand, + HangupCommandDtoTest.expectedHangupCommand)) + .setIdempotencyKey("7b2e9d41-5c8a-4f3e-a1d6-0e4b8c2f9a17") + .build(); + + @GivenTextResource("/domains/voice/v2/calls/request/CallPatchRequestDto.json") + String jsonCallPatchRequest; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedCallPatchRequest); + + JSONAssert.assertEquals(jsonCallPatchRequest, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/CallPatchRequestDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/CallPatchRequestDto.json new file mode 100644 index 000000000..d0d7b16f8 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/CallPatchRequestDto.json @@ -0,0 +1,36 @@ +{ + "commands": [ + { + "command": "messages", + "messagesName": "my-messages", + "messages": [ + { + "type": "SAY", + "say": { + "text": "Hello, your call is now connected.", + "format": "TEXT", + "voiceName": "Emma" + } + }, + { + "type": "PLAY", + "play": { + "url": "https://example.com/audio/welcome.mp3" + } + } + ], + "events": { + "onFinish": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + }, + { + "command": "hangup", + "callName": "origin" + } + ] +} From b6fad0df41b87d542f5d8c20ff3c9f2be814d5b3 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 11:25:44 +0200 Subject: [PATCH 29/50] Add Voice v2 calls.interactByCallName endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/CallsServiceTest.java | 98 +++++++++++++++++++ .../sdk/e2e/domains/voice/v2/CallsSteps.java | 26 +++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 1 + .../snippets/src/main/java/voice/README.md | 1 + .../voice/v2/calls/InteractByCallName.java | 58 +++++++++++ .../domains/voice/api/v2/CallsService.java | 17 ++++ .../api/v2/adapters/CallsServiceImpl.java | 92 +++++++++++++++++ 8 files changed, 295 insertions(+) create mode 100644 examples/snippets/src/main/java/voice/v2/calls/InteractByCallName.java diff --git a/CHANGELOG.md b/CHANGELOG.md index bf0ac695c..935647331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Calls API: `get` operation to retrieve a call - **[feature]** Voice V2 Calls API: `list` operation to list and filter calls - **[feature]** Voice V2 Calls API: `interactByCallId` operation to send SVAML commands to an ongoing call +- **[feature]** Voice V2 Calls API: `interactByCallName` operation to send SVAML commands to an ongoing call identified by session ID and call name - **[feature]** Voice V2 Batches API: `start` operation to create a batch of outbound calls - **[feature]** Voice V2 Batches API: `get` operation to retrieve a batch summary - **[feature]** Voice V2 Batches API: `getDetails` operation to retrieve the per-session details of a batch @@ -33,6 +34,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/calls/Get` snippet - **[doc]** New `voice/v2/calls/List` snippet - **[doc]** New `voice/v2/calls/InteractByCallId` snippet +- **[doc]** New `voice/v2/calls/InteractByCallName` snippet - **[doc]** New `voice/v2/batches/Start` snippet - **[doc]** New `voice/v2/batches/Get` snippet - **[doc]** New `voice/v2/batches/GetDetails` snippet diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java index 36d534af8..ec0b94fb5 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java @@ -402,4 +402,102 @@ void interactByCallIdMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + + @Test + void interactByCallName() throws ApiException { + + String sessionId = "01ARZ3NDEKTSV4RRFFQ69G5FAV"; + String callName = "origin"; + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/sessions/" + + URLPathUtils.encodePathSegment(sessionId) + + "/calls/" + + URLPathUtils.encodePathSegment(callName), + HttpMethod.PATCH, + Collections.emptyList(), + jsonCallPatchRequestDto, + Collections.singletonMap( + "Idempotency-Key", + CallPatchRequestDtoTest.expectedCallPatchRequest.getIdempotencyKey()), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = new HttpResponse(202, null, Collections.emptyMap(), null); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + service.interactByCallName( + sessionId, callName, CallPatchRequestDtoTest.expectedCallPatchRequest); + + verify(httpClient) + .invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest))); + } + + @Test + void interactByCallNameMissingSessionIdThrows() { + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> + service.interactByCallName( + null, "origin", CallPatchRequestDtoTest.expectedCallPatchRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void interactByCallNameMissingCallNameThrows() { + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> + service.interactByCallName( + "01ARZ3NDEKTSV4RRFFQ69G5FAV", + null, + CallPatchRequestDtoTest.expectedCallPatchRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void interactByCallNameMissingRequestThrows() { + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> service.interactByCallName("01ARZ3NDEKTSV4RRFFQ69G5FAV", "origin", null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void interactByCallNameMissingProjectIdThrows() { + + CallsService serviceWithoutProjectId = + new CallsServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> + serviceWithoutProjectId.interactByCallName( + "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "origin", + CallPatchRequestDtoTest.expectedCallPatchRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java index b97ad6560..86a2cd537 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/CallsSteps.java @@ -199,6 +199,32 @@ public void interactByCallId() { interactionAccepted = true; } + @When("^I send a request to interact with an ongoing call by call name$") + public void interactByCallName() { + MessagesCommand messages = + MessagesCommand.builder() + .setMessages( + Collections.singletonList( + SayMessage.builder() + .setText("Hello, your call is now connected.") + .setVoiceName("Emma") + .build())) + .setEvents( + MessageEvents.builder() + .setOnFinish(Collections.singletonList(HangupCommand.builder().build())) + .build()) + .build(); + + service.interactByCallName( + "01HZXK8FQNPMR8VD3JW9YF2C5B", + "origin", + CallPatchRequest.builder() + .setCommands(Collections.singletonList(messages)) + .setIdempotencyKey("7c1e9b4a-3f2d-4e8a-9b6c-0d5f8a2e1c37") + .build()); + interactionAccepted = true; + } + @Then("^the response confirms the interaction request was accepted$") public void interactResult() { Assertions.assertTrue(interactionAccepted); diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 02e7be382..7c5cc8bf4 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -34,6 +34,7 @@ + "|^\\[Get\\] get call details$" + "|^\\[List\\] list (a page of|all the) calls.*$" + "|^\\[InteractByCallId\\] interact with an ongoing call by call ID$" + + "|^\\[InteractByCallName\\] interact with an ongoing call by call name$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 90d84530b..2328d8530 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -28,6 +28,7 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/calls/Get](./v2/calls/Get.java) - [voice/v2/calls/List](./v2/calls/List.java) - [voice/v2/calls/InteractByCallId](./v2/calls/InteractByCallId.java) + - [voice/v2/calls/InteractByCallName](./v2/calls/InteractByCallName.java) - Batches - [voice/v2/batches/Start](./v2/batches/Start.java) - [voice/v2/batches/Get](./v2/batches/Get.java) diff --git a/examples/snippets/src/main/java/voice/v2/calls/InteractByCallName.java b/examples/snippets/src/main/java/voice/v2/calls/InteractByCallName.java new file mode 100644 index 000000000..3208c2931 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/calls/InteractByCallName.java @@ -0,0 +1,58 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.calls; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; +import com.sinch.sdk.models.Configuration; +import java.util.Collections; +import java.util.logging.Logger; +import utils.Settings; + +public class InteractByCallName { + + private static final Logger LOGGER = Logger.getLogger(InteractByCallName.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the session that contains the call + String sessionId = "SESSION_ID"; + // The name of the call leg within the session, as assigned by the `callName` property in the + // `dial` command + String callName = "CALL_NAME"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + CallsService callsService = client.voice().v2().calls(); + + CallPatchRequest request = + CallPatchRequest.builder() + .setCommands(Collections.singletonList(HangupCommand.HANGUP_COMMAND)) + .build(); + + LOGGER.info( + String.format("Interact with call named '%s' in session '%s'", callName, sessionId)); + + callsService.interactByCallName(sessionId, callName, request); + + LOGGER.info("Commands accepted for processing"); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java index 57f031fe6..15a22904a 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java @@ -85,4 +85,21 @@ StartCallResponse start( * @since 2.3 */ void interactByCallId(String callId, CallPatchRequest callPatchRequest) throws ApiException; + + /** + * Patch an ongoing call by session ID and call name + * + *

Interact with an ongoing call identified by its session and call name by submitting a set of + * SVAML commands. Use this to force disconnect, play messages, bridge with another call, or + * perform other call control actions. + * + * @param sessionId The ID of the session. (required) + * @param callName The name of the call leg within the session, as assigned by the callName + * property in the dial command. (required) + * @param callPatchRequest The SVAML commands to apply to the call (required) + * @throws ApiException if fails to make API call + * @since 2.3 + */ + void interactByCallName(String sessionId, String callName, CallPatchRequest callPatchRequest) + throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java index fc6c9a32d..ce21ba9d5 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java @@ -472,4 +472,96 @@ private HttpRequest interactByCallIdRequestBuilder( localVarContentTypes, localVarAuthNames); } + + @Override + public void interactByCallName( + String sessionId, String callName, CallPatchRequest callPatchRequest) throws ApiException { + + LOGGER.finest( + "[interactByCallName]" + + " " + + "sessionId: " + + sessionId + + ", " + + "callName: " + + callName + + ", " + + "callPatchRequest: " + + callPatchRequest); + + HttpRequest httpRequest = + interactByCallNameRequestBuilder(sessionId, callName, callPatchRequest); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return; + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest interactByCallNameRequestBuilder( + String sessionId, String callName, CallPatchRequest callPatchRequest) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling interactByCallName"); + } + // verify the required parameter 'sessionId' is set + if (sessionId == null) { + throw new ApiException( + 400, "Missing the required parameter 'sessionId' when calling interactByCallName"); + } + // verify the required parameter 'callName' is set + if (callName == null) { + throw new ApiException( + 400, "Missing the required parameter 'callName' when calling interactByCallName"); + } + // verify the required parameter 'callPatchRequest' is set + if (callPatchRequest == null) { + throw new ApiException( + 400, "Missing the required parameter 'callPatchRequest' when calling interactByCallName"); + } + + String localVarPath = + "/v2/projects/{projectId}/sessions/{sessionId}/calls/{callName}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "sessionId" + "\\}", URLPathUtils.encodePathSegment(sessionId.toString())) + .replaceAll( + "\\{" + "callName" + "\\}", URLPathUtils.encodePathSegment(callName.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + if (callPatchRequest.getIdempotencyKey() != null) + localVarHeaderParams.put("Idempotency-Key", callPatchRequest.getIdempotencyKey()); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList("application/json"); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = mapper.serialize(localVarContentTypes, callPatchRequest); + + return new HttpRequest( + localVarPath, + HttpMethod.PATCH, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } From 1e5578e2f8d9dbd7b76266363228dcf556363521 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 11:48:42 +0200 Subject: [PATCH 30/50] DEVEXP-1624: Add VoiceSinchEventResponse and rename SvamlInputEvents to IncomingCallResponseEvents --- .../voice/api/v2/SinchEventsService.java | 7 +- .../api/v2/adapters/SinchEventsService.java | 4 +- .../v2/adapters/SinchEventsServiceTest.java | 11 +- .../domains/voice/v2/SinchEventsSteps.java | 6 +- .../mycompany/app/voice/v2/Controller.java | 4 +- .../app/voice/v2/ServerBusinessLogic.java | 18 +-- .../sinchevents/VoiceSinchEventResponse.java | 106 +++++++++++++ .../VoiceSinchEventResponseImpl.java | 150 ++++++++++++++++++ ...s.java => IncomingCallResponseEvents.java} | 8 +- ...va => IncomingCallResponseEventsImpl.java} | 22 +-- .../voice/models/v2/svaml/SvamlInput.java | 8 +- .../voice/models/v2/svaml/SvamlInputImpl.java | 12 +- .../VoiceSinchEventResponseDtoTest.java | 33 ++++ ...=> IncomingCallResponseEventsDtoTest.java} | 14 +- .../models/v2/svaml/SvamlInputDtoTest.java | 2 +- .../VoiceSinchEventResponseDto.json | 17 ++ ...son => IncomingCallResponseEventsDto.json} | 0 17 files changed, 362 insertions(+), 60 deletions(-) create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponseImpl.java rename openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/{SvamlInputEvents.java => IncomingCallResponseEvents.java} (78%) rename openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/{SvamlInputEventsImpl.java => IncomingCallResponseEventsImpl.java} (72%) create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponseDtoTest.java rename openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/{SvamlInputEventsDtoTest.java => IncomingCallResponseEventsDtoTest.java} (63%) create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventResponseDto.json rename openapi-contracts/src/test/resources/domains/voice/v2/svaml/{SvamlInputEventsDto.json => IncomingCallResponseEventsDto.json} (100%) diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/SinchEventsService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/SinchEventsService.java index 89a3d57f9..02cebb547 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/SinchEventsService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/SinchEventsService.java @@ -2,7 +2,7 @@ import com.sinch.sdk.core.exceptions.ApiMappingException; import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; -import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse; import java.util.Map; /** @@ -79,13 +79,12 @@ boolean validateAuthenticationHeader( VoiceSinchEvent parseEvent(String jsonPayload) throws ApiMappingException; /** - * This function can be called to serialize the SVAML document to be sent in response to a Sinch - * Event + * This function can be called to serialize the response to a Sinch Event * * @param response The response to be serialized * @return The JSON string to be sent * @throws ApiMappingException if the response cannot be serialized * @since 2.3 */ - String serializeResponse(SvamlInput response) throws ApiMappingException; + String serializeResponse(VoiceSinchEventResponse response) throws ApiMappingException; } diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsService.java index c6847fcf2..6101aa712 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsService.java @@ -10,7 +10,7 @@ import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.core.utils.databind.Mapper; import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; -import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Locale; @@ -116,7 +116,7 @@ public VoiceSinchEvent parseEvent(String jsonPayload) throws ApiMappingException } @Override - public String serializeResponse(SvamlInput response) throws ApiMappingException { + public String serializeResponse(VoiceSinchEventResponse response) throws ApiMappingException { try { return Mapper.getInstance().writeValueAsString(response); } catch (JsonProcessingException e) { diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsServiceTest.java index 127a27c13..2c806f485 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SinchEventsServiceTest.java @@ -10,7 +10,7 @@ import com.sinch.sdk.domains.voice.models.v2.sinchevents.SinchEventType; import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventDtoTest; -import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInputDtoTest; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponseDtoTest; import java.util.HashMap; import java.util.Map; import org.assertj.core.api.Assertions; @@ -36,8 +36,8 @@ class SinchEventsServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/sinchevents/VoiceSinchEventCustomEventDto.json") String jsonVoiceSinchEventCustomEvent; - @GivenTextResource("/domains/voice/v2/svaml/SvamlInputDto.json") - String jsonSvamlInput; + @GivenTextResource("/domains/voice/v2/sinchevents/VoiceSinchEventResponseDto.json") + String jsonVoiceSinchEventResponse; SinchEventsService service; @@ -246,8 +246,9 @@ void parseEventInvalidPayload() { @Test void serializeResponse() throws ApiMappingException, JSONException { - String serialized = service.serializeResponse(SvamlInputDtoTest.expectedSvamlInput); + String serialized = + service.serializeResponse(VoiceSinchEventResponseDtoTest.expectedVoiceSinchEventResponse); - JSONAssert.assertEquals(jsonSvamlInput, serialized, true); + JSONAssert.assertEquals(jsonVoiceSinchEventResponse, serialized, true); } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SinchEventsSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SinchEventsSteps.java index 05eb0fcd1..6aeb7fb61 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SinchEventsSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SinchEventsSteps.java @@ -11,7 +11,7 @@ import com.sinch.sdk.domains.voice.models.v2.destination.Phone; import com.sinch.sdk.domains.voice.models.v2.sinchevents.SinchEventType; import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; -import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse; import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; @@ -95,8 +95,8 @@ static Call.Builder baseCall() { .build()); } - static final SvamlInput onAnswerResponse = - SvamlInput.builder() + static final VoiceSinchEventResponse onAnswerResponse = + VoiceSinchEventResponse.builder() .setCommands( Collections.singletonList( MessagesCommand.builder() diff --git a/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/Controller.java b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/Controller.java index 63301988e..72d05d8c8 100644 --- a/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/Controller.java +++ b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/Controller.java @@ -4,7 +4,7 @@ import com.sinch.sdk.domains.voice.api.v2.SinchEventsService; import com.sinch.sdk.domains.voice.models.v2.sinchevents.SinchEventType; import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; -import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse; import java.util.Map; import java.util.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; @@ -82,7 +82,7 @@ public ResponseEntity voiceV2Event( // let business layer process the request SinchEventType type = event.getEvent(); - SvamlInput response; + VoiceSinchEventResponse response; if (SinchEventType.CALL_INCOMING.equals(type)) { response = serverBusinessLogic.incoming(event); } else if (SinchEventType.CALL_ANSWERED.equals(type)) { diff --git a/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java index 01e50e4dc..2148b47b2 100644 --- a/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java +++ b/examples/sinch-events/src/main/java/com/mycompany/app/voice/v2/ServerBusinessLogic.java @@ -1,7 +1,7 @@ package com.mycompany.app.voice.v2; import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEvent; -import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.sinchevents.VoiceSinchEventResponse; import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; @@ -16,16 +16,16 @@ public class ServerBusinessLogic { private static final Logger LOGGER = Logger.getLogger(ServerBusinessLogic.class.getName()); // An empty list of commands takes no action - private static final SvamlInput NO_ACTION = - SvamlInput.builder().setCommands(Collections.emptyList()).build(); + private static final VoiceSinchEventResponse NO_ACTION = + VoiceSinchEventResponse.builder().setCommands(Collections.emptyList()).build(); - public SvamlInput incoming(VoiceSinchEvent event) { + public VoiceSinchEventResponse incoming(VoiceSinchEvent event) { LOGGER.info("Handle event: " + event); // play a greeting, then hang up // "callName" and "events" only take effect in responses to "call.incoming" events - return SvamlInput.builder() + return VoiceSinchEventResponse.builder() .setCallName("incoming") .setCommands( Collections.singletonList( @@ -44,14 +44,14 @@ public SvamlInput incoming(VoiceSinchEvent event) { .build(); } - public SvamlInput answered(VoiceSinchEvent event) { + public VoiceSinchEventResponse answered(VoiceSinchEvent event) { LOGGER.info("Handle event: " + event); return NO_ACTION; } - public SvamlInput menu(VoiceSinchEvent event) { + public VoiceSinchEventResponse menu(VoiceSinchEvent event) { // event.getMenu() holds the menu name and the input gathered from the user LOGGER.info("Handle event: " + event); @@ -59,14 +59,14 @@ public SvamlInput menu(VoiceSinchEvent event) { return NO_ACTION; } - public SvamlInput customEvent(VoiceSinchEvent event) { + public VoiceSinchEventResponse customEvent(VoiceSinchEvent event) { LOGGER.info("Handle event: " + event); return NO_ACTION; } - public SvamlInput other(VoiceSinchEvent event) { + public VoiceSinchEventResponse other(VoiceSinchEvent event) { LOGGER.info("Handle event: " + event); diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponse.java new file mode 100644 index 000000000..126ccff67 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponse.java @@ -0,0 +1,106 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.svaml.IncomingCallResponseEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand; +import java.util.List; + +/** + * A SVAML document returned in response to a Sinch Event. + * + *

Note: callName and events only take effect in + * responses to events triggered by an incoming call. In responses to other event types, they are + * ignored. + */ +@JsonDeserialize(builder = VoiceSinchEventResponseImpl.Builder.class) +public interface VoiceSinchEventResponse { + + /** + * The ordered list of SVAML commands to execute. Contains at least one command. + * + *

Blocking vs. non-blocking: Some commands block execution until they complete (pause + * , webhook, menu, gotoMenu), while others return + * immediately and run in parallel (dial, messages, amd, + * answer, hangup, startRecording, stopRecording + * , bridgeCall, stopMessages). Each command's description + * specifies its behavior. + * + *

Nesting scope: Commands that appear inside event handlers (e.g., dial.events.onAnswer + * , messages.events.onFinish) form independent sequences and execute in their + * own context — they are not continuations of the parent sequence. + * + *

Field is required + * + * @return commands + */ + List getCommands(); + + /** + * Name of the call. + * + *

Must be 1-32 characters, without whitespace. + * + *

Note: only takes effect in responses to events triggered by an incoming + * call. In responses to other event types, it is ignored. + * + * @return callName + */ + String getCallName(); + + /** + * Commands to execute on specific events for this call. + * + *

Note: only takes effect in responses to events triggered by an incoming + * call. In responses to other event types, it is ignored. + * + * @return events + */ + IncomingCallResponseEvents getEvents(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new VoiceSinchEventResponseImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param commands see getter + * @return Current builder + * @see #getCommands + */ + Builder setCommands(List commands); + + /** + * see getter + * + * @param callName see getter + * @return Current builder + * @see #getCallName + */ + Builder setCallName(String callName); + + /** + * see getter + * + * @param events see getter + * @return Current builder + * @see #getEvents + */ + Builder setEvents(IncomingCallResponseEvents events); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + VoiceSinchEventResponse build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponseImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponseImpl.java new file mode 100644 index 000000000..1f9ba57f3 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponseImpl.java @@ -0,0 +1,150 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.svaml.IncomingCallResponseEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlCommand; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + VoiceSinchEventResponseImpl.JSON_PROPERTY_COMMANDS, + VoiceSinchEventResponseImpl.JSON_PROPERTY_CALL_NAME, + VoiceSinchEventResponseImpl.JSON_PROPERTY_EVENTS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class VoiceSinchEventResponseImpl implements VoiceSinchEventResponse { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_COMMANDS = "commands"; + + private OptionalValue> commands; + + public static final String JSON_PROPERTY_CALL_NAME = "callName"; + + private OptionalValue callName; + + public static final String JSON_PROPERTY_EVENTS = "events"; + + private OptionalValue events; + + public VoiceSinchEventResponseImpl() {} + + protected VoiceSinchEventResponseImpl( + OptionalValue> commands, + OptionalValue callName, + OptionalValue events) { + this.commands = commands; + this.callName = callName; + this.events = events; + } + + @JsonIgnore + public List getCommands() { + return commands.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_COMMANDS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> commands() { + return commands; + } + + @JsonIgnore + public String getCallName() { + return callName.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callName() { + return callName; + } + + @JsonIgnore + public IncomingCallResponseEvents getEvents() { + return events.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue events() { + return events; + } + + /** Return true if this VoiceSinchEventResponse object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VoiceSinchEventResponseImpl voiceSinchEventResponse = (VoiceSinchEventResponseImpl) o; + return Objects.equals(this.commands, voiceSinchEventResponse.commands) + && Objects.equals(this.callName, voiceSinchEventResponse.callName) + && Objects.equals(this.events, voiceSinchEventResponse.events); + } + + @Override + public int hashCode() { + return Objects.hash(commands, callName, events); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class VoiceSinchEventResponseImpl {\n"); + sb.append(" commands: ").append(toIndentedString(commands)).append("\n"); + sb.append(" callName: ").append(toIndentedString(callName)).append("\n"); + sb.append(" events: ").append(toIndentedString(events)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements VoiceSinchEventResponse.Builder { + OptionalValue> commands = OptionalValue.empty(); + OptionalValue callName = OptionalValue.empty(); + OptionalValue events = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_COMMANDS, required = true) + public Builder setCommands(List commands) { + this.commands = OptionalValue.of(commands); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_NAME) + public Builder setCallName(String callName) { + this.callName = OptionalValue.of(callName); + return this; + } + + @JsonProperty(JSON_PROPERTY_EVENTS) + public Builder setEvents(IncomingCallResponseEvents events) { + this.events = OptionalValue.of(events); + return this; + } + + public VoiceSinchEventResponse build() { + return new VoiceSinchEventResponseImpl(commands, callName, events); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEvents.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEvents.java similarity index 78% rename from openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEvents.java rename to openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEvents.java index 15056781c..2888d55c2 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEvents.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEvents.java @@ -4,8 +4,8 @@ import java.util.List; /** Commands to execute on specific events for this call. */ -@JsonDeserialize(builder = SvamlInputEventsImpl.Builder.class) -public interface SvamlInputEvents { +@JsonDeserialize(builder = IncomingCallResponseEventsImpl.Builder.class) +public interface IncomingCallResponseEvents { /** * SVAML commands to be executed when the call is hung up. @@ -20,7 +20,7 @@ public interface SvamlInputEvents { * @return New Builder instance */ static Builder builder() { - return new SvamlInputEventsImpl.Builder(); + return new IncomingCallResponseEventsImpl.Builder(); } /** Dedicated Builder */ @@ -40,6 +40,6 @@ interface Builder { * * @return The instance build with current builder values */ - SvamlInputEvents build(); + IncomingCallResponseEvents build(); } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEventsImpl.java similarity index 72% rename from openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsImpl.java rename to openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEventsImpl.java index cf90b9e09..8e2fbaa2c 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEventsImpl.java @@ -10,19 +10,19 @@ import java.util.List; import java.util.Objects; -@JsonPropertyOrder({SvamlInputEventsImpl.JSON_PROPERTY_ON_HANGUP}) +@JsonPropertyOrder({IncomingCallResponseEventsImpl.JSON_PROPERTY_ON_HANGUP}) @JsonFilter("uninitializedFilter") @JsonInclude(value = JsonInclude.Include.CUSTOM) -public class SvamlInputEventsImpl implements SvamlInputEvents { +public class IncomingCallResponseEventsImpl implements IncomingCallResponseEvents { private static final long serialVersionUID = 1L; public static final String JSON_PROPERTY_ON_HANGUP = "onHangup"; private OptionalValue> onHangup; - public SvamlInputEventsImpl() {} + public IncomingCallResponseEventsImpl() {} - protected SvamlInputEventsImpl(OptionalValue> onHangup) { + protected IncomingCallResponseEventsImpl(OptionalValue> onHangup) { this.onHangup = onHangup; } @@ -37,7 +37,7 @@ public OptionalValue> onHangup() { return onHangup; } - /** Return true if this SvamlInputEvents object is equal to o. */ + /** Return true if this IncomingCallResponseEvents object is equal to o. */ @Override public boolean equals(Object o) { if (this == o) { @@ -46,8 +46,8 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - SvamlInputEventsImpl svamlInputEvents = (SvamlInputEventsImpl) o; - return Objects.equals(this.onHangup, svamlInputEvents.onHangup); + IncomingCallResponseEventsImpl incomingCallResponseEvents = (IncomingCallResponseEventsImpl) o; + return Objects.equals(this.onHangup, incomingCallResponseEvents.onHangup); } @Override @@ -58,7 +58,7 @@ public int hashCode() { @Override public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("class SvamlInputEventsImpl {\n"); + sb.append("class IncomingCallResponseEventsImpl {\n"); sb.append(" onHangup: ").append(toIndentedString(onHangup)).append("\n"); sb.append("}"); return sb.toString(); @@ -75,7 +75,7 @@ private String toIndentedString(Object o) { } @JsonPOJOBuilder(withPrefix = "set") - static class Builder implements SvamlInputEvents.Builder { + static class Builder implements IncomingCallResponseEvents.Builder { OptionalValue> onHangup = OptionalValue.empty(); @JsonProperty(JSON_PROPERTY_ON_HANGUP) @@ -84,8 +84,8 @@ public Builder setOnHangup(List onHangup) { return this; } - public SvamlInputEvents build() { - return new SvamlInputEventsImpl(onHangup); + public IncomingCallResponseEvents build() { + return new IncomingCallResponseEventsImpl(onHangup); } } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java index 0a8cb953f..fb5757a94 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInput.java @@ -6,10 +6,6 @@ /** * A SVAML v2 (Sinch Voice Application Markup Language) document describing a call flow. Contains * the ordered list of commands to execute, an optional call name, and optional event handlers. - * - *

When returned in response to a Sinch Event, callName and events only - * take effect in responses to events triggered by an incoming call. In responses to other event - * types, they are ignored. */ @JsonDeserialize(builder = SvamlInputImpl.Builder.class) public interface SvamlInput { @@ -48,7 +44,7 @@ public interface SvamlInput { * * @return events */ - SvamlInputEvents getEvents(); + IncomingCallResponseEvents getEvents(); /** * Getting builder @@ -87,7 +83,7 @@ interface Builder { * @return Current builder * @see #getEvents */ - Builder setEvents(SvamlInputEvents events); + Builder setEvents(IncomingCallResponseEvents events); /** * Create instance diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputImpl.java index ae2aa3577..c42477476 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputImpl.java @@ -30,14 +30,14 @@ public class SvamlInputImpl implements SvamlInput { public static final String JSON_PROPERTY_EVENTS = "events"; - private OptionalValue events; + private OptionalValue events; public SvamlInputImpl() {} protected SvamlInputImpl( OptionalValue> commands, OptionalValue callName, - OptionalValue events) { + OptionalValue events) { this.commands = commands; this.callName = callName; this.events = events; @@ -66,13 +66,13 @@ public OptionalValue callName() { } @JsonIgnore - public SvamlInputEvents getEvents() { + public IncomingCallResponseEvents getEvents() { return events.orElse(null); } @JsonProperty(JSON_PROPERTY_EVENTS) @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) - public OptionalValue events() { + public OptionalValue events() { return events; } @@ -121,7 +121,7 @@ private String toIndentedString(Object o) { static class Builder implements SvamlInput.Builder { OptionalValue> commands = OptionalValue.empty(); OptionalValue callName = OptionalValue.empty(); - OptionalValue events = OptionalValue.empty(); + OptionalValue events = OptionalValue.empty(); @JsonProperty(value = JSON_PROPERTY_COMMANDS, required = true) public Builder setCommands(List commands) { @@ -136,7 +136,7 @@ public Builder setCallName(String callName) { } @JsonProperty(JSON_PROPERTY_EVENTS) - public Builder setEvents(SvamlInputEvents events) { + public Builder setEvents(IncomingCallResponseEvents events) { this.events = OptionalValue.of(events); return this; } diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponseDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponseDtoTest.java new file mode 100644 index 000000000..f93ed1f8c --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/sinchevents/VoiceSinchEventResponseDtoTest.java @@ -0,0 +1,33 @@ +package com.sinch.sdk.domains.voice.models.v2.sinchevents; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.IncomingCallResponseEventsDtoTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommandDtoTest; +import java.util.Arrays; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class VoiceSinchEventResponseDtoTest extends BaseTest { + + public static final VoiceSinchEventResponse expectedVoiceSinchEventResponse = + VoiceSinchEventResponse.builder() + .setCommands(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .setCallName("incoming") + .setEvents(IncomingCallResponseEventsDtoTest.expectedIncomingCallResponseEvents) + .build(); + + @GivenTextResource("/domains/voice/v2/sinchevents/VoiceSinchEventResponseDto.json") + String jsonVoiceSinchEventResponse; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedVoiceSinchEventResponse); + + JSONAssert.assertEquals(jsonVoiceSinchEventResponse, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEventsDtoTest.java similarity index 63% rename from openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsDtoTest.java rename to openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEventsDtoTest.java index 3bb1ee4a2..217716093 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputEventsDtoTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/IncomingCallResponseEventsDtoTest.java @@ -11,20 +11,20 @@ import org.skyscreamer.jsonassert.JSONAssert; @TestWithResources -public class SvamlInputEventsDtoTest extends BaseTest { +public class IncomingCallResponseEventsDtoTest extends BaseTest { - public static final SvamlInputEvents expectedSvamlInputEvents = - SvamlInputEvents.builder() + public static final IncomingCallResponseEvents expectedIncomingCallResponseEvents = + IncomingCallResponseEvents.builder() .setOnHangup(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) .build(); - @GivenTextResource("/domains/voice/v2/svaml/SvamlInputEventsDto.json") - String jsonSvamlInputEvents; + @GivenTextResource("/domains/voice/v2/svaml/IncomingCallResponseEventsDto.json") + String jsonIncomingCallResponseEvents; @Test void serialize() throws JsonProcessingException, JSONException { - String serializedString = objectMapper.writeValueAsString(expectedSvamlInputEvents); + String serializedString = objectMapper.writeValueAsString(expectedIncomingCallResponseEvents); - JSONAssert.assertEquals(jsonSvamlInputEvents, serializedString, true); + JSONAssert.assertEquals(jsonIncomingCallResponseEvents, serializedString, true); } } diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputDtoTest.java index f54b4ef2a..b939844b8 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputDtoTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/SvamlInputDtoTest.java @@ -17,7 +17,7 @@ public class SvamlInputDtoTest extends BaseTest { SvamlInput.builder() .setCommands(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) .setCallName("incoming") - .setEvents(SvamlInputEventsDtoTest.expectedSvamlInputEvents) + .setEvents(IncomingCallResponseEventsDtoTest.expectedIncomingCallResponseEvents) .build(); @GivenTextResource("/domains/voice/v2/svaml/SvamlInputDto.json") diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventResponseDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventResponseDto.json new file mode 100644 index 000000000..7f6fd301e --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/sinchevents/VoiceSinchEventResponseDto.json @@ -0,0 +1,17 @@ +{ + "commands": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "callName": "incoming", + "events": { + "onHangup": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputEventsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/IncomingCallResponseEventsDto.json similarity index 100% rename from openapi-contracts/src/test/resources/domains/voice/v2/svaml/SvamlInputEventsDto.json rename to openapi-contracts/src/test/resources/domains/voice/v2/svaml/IncomingCallResponseEventsDto.json From 5c54b521362fccaa500031735c0b8ea1ca5c190b Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 13:21:05 +0200 Subject: [PATCH 31/50] Add Voice v2 services.create endpoint --- CHANGELOG.md | 2 + .../domains/voice/api/v2/VoiceService.java | 8 + .../voice/api/v2/adapters/VoiceService.java | 20 ++ .../models/v2/services/CallBehavior.java | 13 + .../models/v2/services/package-info.java | 6 + .../v2/services/request/package-info.java | 6 + .../v2/services/response/package-info.java | 6 + .../api/v2/adapters/ServicesServiceTest.java | 150 +++++++++ .../api/v2/adapters/VoiceServiceTest.java | 5 + .../e2e/domains/voice/v2/ServicesSteps.java | 50 +++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 2 + .../snippets/src/main/java/voice/README.md | 2 + .../main/java/voice/v2/services/Create.java | 49 +++ .../domains/voice/api/v2/ServicesService.java | 21 ++ .../api/v2/adapters/ServicesServiceImpl.java | 114 +++++++ .../EventDestinationCallBehavior.java | 51 ++++ .../EventDestinationCallBehaviorImpl.java | 152 ++++++++++ .../EventDestinationConfiguration.java | 68 +++++ .../EventDestinationConfigurationImpl.java | 120 ++++++++ .../models/v2/services/NoneCallBehavior.java | 34 +++ .../v2/services/NoneCallBehaviorImpl.java | 121 ++++++++ .../v2/services/StaticCallBehavior.java | 50 +++ .../v2/services/StaticCallBehaviorImpl.java | 151 ++++++++++ .../internal/CallBehaviorInternal.java | 6 + .../internal/CallBehaviorInternalImpl.java | 178 +++++++++++ .../request/CreateServiceRequest.java | 117 ++++++++ .../request/CreateServiceRequestImpl.java | 199 ++++++++++++ .../v2/services/response/ServiceResponse.java | 177 +++++++++++ .../response/ServiceResponseImpl.java | 284 ++++++++++++++++++ .../EventDestinationCallBehaviorDtoTest.java | 47 +++ .../EventDestinationConfigurationDtoTest.java | 40 +++ .../v2/services/NoneCallBehaviorDtoTest.java | 42 +++ .../services/StaticCallBehaviorDtoTest.java | 43 +++ .../request/CreateServiceRequestDtoTest.java | 33 ++ .../response/ServiceResponseDtoTest.java | 57 ++++ .../EventDestinationCallBehaviorDto.json | 7 + .../EventDestinationConfigurationDto.json | 4 + .../v2/services/NoneCallBehaviorDto.json | 3 + .../v2/services/StaticCallBehaviorDto.json | 20 ++ .../request/CreateServiceRequestDto.json | 12 + .../services/response/ServiceResponseDto.json | 16 + .../response/ServiceResponseMinimalDto.json | 7 + 42 files changed, 2493 insertions(+) create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/services/CallBehavior.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/services/package-info.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/package-info.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/package-info.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java create mode 100644 client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java create mode 100644 examples/snippets/src/main/java/voice/v2/services/Create.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehavior.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehaviorImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfiguration.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfigurationImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehavior.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehaviorImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehavior.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehaviorImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/internal/CallBehaviorInternal.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/internal/CallBehaviorInternalImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequest.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequestImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehaviorDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfigurationDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehaviorDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehaviorDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequestDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/EventDestinationCallBehaviorDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/EventDestinationConfigurationDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/NoneCallBehaviorDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/StaticCallBehaviorDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/request/CreateServiceRequestDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseMinimalDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 935647331..6fac56318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Batches API: `getDetails` operation to retrieve the per-session details of a batch - **[feature]** Voice V2 Batches API: `stop` operation to stop processing a batch of calls - **[feature]** Voice V2 Sessions API: `get` operation to retrieve a session and its calls +- **[feature]** Voice V2 Services API: `create` operation to create a voice service - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet @@ -40,6 +41,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/batches/GetDetails` snippet - **[doc]** New `voice/v2/batches/Stop` snippet - **[doc]** New `voice/v2/sessions/Get` snippet +- **[doc]** New `voice/v2/services/Create` snippet - **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java index 8a331b553..f18e67910 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java @@ -33,6 +33,14 @@ public interface VoiceService { */ SessionsService sessions(); + /** + * Services Service instance + * + * @return service instance for project + * @since 2.3 + */ + ServicesService services(); + /** * Sinch Events Service instance * diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java index 48e55d6eb..c78cef676 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java @@ -8,6 +8,7 @@ import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.domains.voice.api.v2.BatchesService; import com.sinch.sdk.domains.voice.api.v2.CallsService; +import com.sinch.sdk.domains.voice.api.v2.ServicesService; import com.sinch.sdk.domains.voice.api.v2.SessionsService; import com.sinch.sdk.domains.voice.models.v2.VoiceContext; import com.sinch.sdk.models.UnifiedCredentials; @@ -35,6 +36,7 @@ public class VoiceService implements com.sinch.sdk.domains.voice.api.v2.VoiceSer private volatile CallsService calls; private volatile BatchesService batches; private volatile SessionsService sessions; + private volatile ServicesService services; private volatile SinchEventsService sinchEvents; public VoiceService( @@ -102,6 +104,24 @@ public SessionsService sessions() { return this.sessions; } + public ServicesService services() { + if (null == this.services) { + synchronized (this) { + if (null == this.services) { + instanceLazyInit(); + this.services = + new ServicesServiceImpl( + httpClientSupplier.get(), + context.getVoiceServer(), + authManagers, + HttpMapper.getInstance(), + uriUUID); + } + } + } + return this.services; + } + public SinchEventsService sinchEvents() { // no lazy init: validating a Sinch Event takes service credentials on each call if (null == this.sinchEvents) { diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/CallBehavior.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/CallBehavior.java new file mode 100644 index 000000000..c764074e4 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/CallBehavior.java @@ -0,0 +1,13 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.services.internal.CallBehaviorInternalImpl; + +/** + * Base interface for the call behavior of a service: defines how calls are handled for this + * service. + * + * @since 2.3 + */ +@JsonDeserialize(using = CallBehaviorInternalImpl.Deserializer.class) +public interface CallBehavior {} diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/package-info.java new file mode 100644 index 000000000..f90081bcd --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 services + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.services; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/package-info.java new file mode 100644 index 000000000..87ffe534e --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 services requests + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.services.request; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/package-info.java new file mode 100644 index 000000000..271d6985a --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 services responses + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.services.response; diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java new file mode 100644 index 000000000..a31932f6c --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java @@ -0,0 +1,150 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpContentType; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.HttpRequestTest.HttpRequestMatcher; +import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.URLPathUtils; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.domains.voice.models.v2.services.NoneCallBehavior; +import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponseDtoTest; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +@TestWithResources +public class ServicesServiceTest extends BaseTest { + + @Mock HttpClient httpClient; + @Mock ServerConfiguration serverConfiguration; + @Mock Map authManagers; + + static final String PROJECT_ID = "test_project_id"; + static final Collection AUTH_NAMES = Arrays.asList("BasicAuth", "SinchOAuth2"); + static final Collection ACCEPTS = + Arrays.asList(HttpContentType.APPLICATION_JSON, "application/problem+json"); + + ServicesService service; + + @GivenTextResource("/domains/voice/v2/services/request/CreateServiceRequestDto.json") + String jsonCreateServiceRequestDto; + + @GivenTextResource("/domains/voice/v2/services/response/ServiceResponseDto.json") + String jsonServiceResponseDto; + + @BeforeEach + public void initMocks() { + service = + new ServicesServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), PROJECT_ID); + } + + @Test + void create() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/services", + HttpMethod.POST, + Collections.emptyList(), + jsonCreateServiceRequestDto, + Collections.singletonMap( + "Idempotency-Key", + CreateServiceRequestDtoTest.expectedCreateServiceRequest.getIdempotencyKey()), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(201, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + ServiceResponse response = + service.create(CreateServiceRequestDtoTest.expectedCreateServiceRequest); + + TestHelpers.recursiveEquals(response, ServiceResponseDtoTest.expectedServiceResponse); + } + + @Test + void createWithoutIdempotencyKeySendsNoHeader() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/services", + HttpMethod.POST, + Collections.emptyList(), + "{\"name\":\"Example service\",\"callBehavior\":{\"type\":\"NONE\"}}", + Collections.emptyMap(), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(201, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + ServiceResponse response = + service.create( + CreateServiceRequest.builder() + .setName("Example service") + .setCallBehavior(NoneCallBehavior.NONE_CALL_BEHAVIOR) + .build()); + + TestHelpers.recursiveEquals(response, ServiceResponseDtoTest.expectedServiceResponse); + } + + @Test + void createMissingRequestThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.create(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void createMissingProjectIdThrows() { + + ServicesService serviceWithoutProjectId = + new ServicesServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> + serviceWithoutProjectId.create( + CreateServiceRequestDtoTest.expectedCreateServiceRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } +} diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java index db7e19fbb..ecf1a9b36 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java @@ -26,6 +26,11 @@ void checkCredentialsSessions() { CredentialsValidationHelper.checkCredentials(() -> httpClient, VoiceService::sessions); } + @Test + void checkCredentialsServices() { + CredentialsValidationHelper.checkCredentials(() -> httpClient, VoiceService::services); + } + @Test void sinchEventsRequireNoCredentials() { Assertions.assertNotNull( diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java new file mode 100644 index 000000000..adea55b09 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java @@ -0,0 +1,50 @@ +package com.sinch.sdk.e2e.domains.voice.v2; + +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import com.sinch.sdk.e2e.Config; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.time.Instant; +import org.junit.jupiter.api.Assertions; + +public class ServicesSteps { + + ServicesService service; + ServiceResponse createResponse; + + @Given("^the Voice-V2 service \"Services\" is available$") + public void serviceAvailable() { + service = Config.getSinchClient().voice().v2().services(); + Assertions.assertNotNull(service, "Voice V2 Services service is not available"); + } + + @When("^I send a request to create a Voice-V2 service$") + public void create() { + createResponse = + service.create( + CreateServiceRequest.builder() + .setName("Example service") + .setIdempotencyKey("9e4b2c7a-1d5f-4a8e-b3c6-2f7d0a9e5b18") + .build()); + } + + @Then("^the response contains the information about the Voice-V2 service created$") + public void createResult() { + ServiceResponse expected = + ServiceResponse.builder() + .setServiceId("1a2b3c4d-5e6f-4789-a012-b3c4d5e6f789") + .setProjectId("c3d4e5f6-a7b8-4901-c234-d5e6f7a8b901") + .setCreateTime(Instant.parse("2026-09-16T10:44:09Z")) + .setUpdateTime(Instant.parse("2026-09-16T10:44:09Z")) + .setName("Example service") + .setDescription("") + .setIsDefault(false) + .build(); + + TestHelpers.recursiveEquals(createResponse, expected); + } +} diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 7c5cc8bf4..7192cb0d0 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -24,6 +24,7 @@ @SelectClasspathResource("features/voice-v2/calls.feature") @SelectClasspathResource("features/voice-v2/batches.feature") @SelectClasspathResource("features/voice-v2/sessions.feature") +@SelectClasspathResource("features/voice-v2/services.feature") @SelectClasspathResource("features/voice-v2/webhooks-events.feature") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.sinch.sdk.e2e.domains.voice.v2") @ConfigurationParameter( @@ -35,6 +36,7 @@ + "|^\\[List\\] list (a page of|all the) calls.*$" + "|^\\[InteractByCallId\\] interact with an ongoing call by call ID$" + "|^\\[InteractByCallName\\] interact with an ongoing call by call name$" + + "|^\\[Create\\] create a Voice-V2 service$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 2328d8530..2667c1f67 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -36,3 +36,5 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/batches/Stop](./v2/batches/Stop.java) - Sessions - [voice/v2/sessions/Get](./v2/sessions/Get.java) + - Services + - [voice/v2/services/Create](./v2/services/Create.java) diff --git a/examples/snippets/src/main/java/voice/v2/services/Create.java b/examples/snippets/src/main/java/voice/v2/services/Create.java new file mode 100644 index 000000000..3e9de61a9 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/services/Create.java @@ -0,0 +1,49 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.services; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class Create { + + private static final Logger LOGGER = Logger.getLogger(Create.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + String name = "Example service"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + ServicesService servicesService = client.voice().v2().services(); + + CreateServiceRequest request = CreateServiceRequest.builder().setName(name).build(); + + LOGGER.info(String.format("Create voice service named '%s'", name)); + + ServiceResponse response = servicesService.create(request); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java new file mode 100644 index 000000000..817d52dfb --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java @@ -0,0 +1,21 @@ +package com.sinch.sdk.domains.voice.api.v2; + +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; + +/** Services Service */ +public interface ServicesService { + + /** + * Create a new voice service + * + *

Creates a new voice service in the specified project. + * + * @param createServiceRequest The service to create (required) + * @return ServiceResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + ServiceResponse create(CreateServiceRequest createServiceRequest) throws ApiException; +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java new file mode 100644 index 000000000..5be13cae8 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java @@ -0,0 +1,114 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.exceptions.ApiExceptionBuilder; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.HttpStatus; +import com.sinch.sdk.core.http.URLParameter; +import com.sinch.sdk.core.http.URLPathUtils; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +public class ServicesServiceImpl implements com.sinch.sdk.domains.voice.api.v2.ServicesService { + + private static final Logger LOGGER = Logger.getLogger(ServicesServiceImpl.class.getName()); + private final HttpClient httpClient; + private final ServerConfiguration serverConfiguration; + private final Map authManagersByOasSecuritySchemes; + private final HttpMapper mapper; + + private final String projectId; + + public ServicesServiceImpl( + HttpClient httpClient, + ServerConfiguration serverConfiguration, + Map authManagersByOasSecuritySchemes, + HttpMapper mapper, + String projectId) { + this.httpClient = httpClient; + this.serverConfiguration = serverConfiguration; + this.authManagersByOasSecuritySchemes = authManagersByOasSecuritySchemes; + this.mapper = mapper; + this.projectId = projectId; + } + + @Override + public ServiceResponse create(CreateServiceRequest createServiceRequest) throws ApiException { + + LOGGER.finest("[create]" + " " + "createServiceRequest: " + createServiceRequest); + + HttpRequest httpRequest = createRequestBuilder(createServiceRequest); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest createRequestBuilder(CreateServiceRequest createServiceRequest) + throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling create"); + } + // verify the required parameter 'createServiceRequest' is set + if (createServiceRequest == null) { + throw new ApiException( + 400, "Missing the required parameter 'createServiceRequest' when calling create"); + } + + String localVarPath = + "/v2/projects/{projectId}/services" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + if (createServiceRequest.getIdempotencyKey() != null) + localVarHeaderParams.put("Idempotency-Key", createServiceRequest.getIdempotencyKey()); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList("application/json"); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = mapper.serialize(localVarContentTypes, createServiceRequest); + + return new HttpRequest( + localVarPath, + HttpMethod.POST, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehavior.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehavior.java new file mode 100644 index 000000000..9658a0794 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehavior.java @@ -0,0 +1,51 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * Calls are handled dynamically by sending Sinch Events to the configured event destination. The + * backend responds with SVAML commands that control the call flow in real time. + * + *

Sent on the wire as the WEBHOOK call behavior. + */ +@JsonDeserialize(builder = EventDestinationCallBehaviorImpl.Builder.class) +public interface EventDestinationCallBehavior extends CallBehavior { + + /** + * Event destination receiving the Sinch Events. + * + *

Field is required + * + * @return eventDestination + */ + EventDestinationConfiguration getEventDestination(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new EventDestinationCallBehaviorImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param eventDestination see getter + * @return Current builder + * @see #getEventDestination + */ + Builder setEventDestination(EventDestinationConfiguration eventDestination); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + EventDestinationCallBehavior build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehaviorImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehaviorImpl.java new file mode 100644 index 000000000..4551798c4 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehaviorImpl.java @@ -0,0 +1,152 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.Objects; + +@JsonPropertyOrder({ + EventDestinationCallBehaviorImpl.JSON_PROPERTY_TYPE, + EventDestinationCallBehaviorImpl.JSON_PROPERTY_WEBHOOK +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class EventDestinationCallBehaviorImpl + implements EventDestinationCallBehavior, CallBehavior { + private static final long serialVersionUID = 1L; + + /** The type property. Must have the value WEBHOOK. */ + public static class TypeEnum extends EnumDynamic { + /** Calls are handled by sending Sinch Events to the configured event destination. */ + public static final TypeEnum WEBHOOK = new TypeEnum("WEBHOOK"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(WEBHOOK)); + + private TypeEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_WEBHOOK = "webhook"; + + private OptionalValue eventDestination; + + public EventDestinationCallBehaviorImpl() {} + + protected EventDestinationCallBehaviorImpl( + OptionalValue type, OptionalValue eventDestination) { + this.type = type; + this.eventDestination = eventDestination; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public EventDestinationConfiguration getEventDestination() { + return eventDestination.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_WEBHOOK) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue eventDestination() { + return eventDestination; + } + + /** Return true if this EventDestinationCallBehavior object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventDestinationCallBehaviorImpl eventDestinationCallBehavior = + (EventDestinationCallBehaviorImpl) o; + return Objects.equals(this.type, eventDestinationCallBehavior.type) + && Objects.equals(this.eventDestination, eventDestinationCallBehavior.eventDestination); + } + + @Override + public int hashCode() { + return Objects.hash(type, eventDestination); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class EventDestinationCallBehaviorImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" eventDestination: ").append(toIndentedString(eventDestination)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements EventDestinationCallBehavior.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.WEBHOOK); + OptionalValue eventDestination = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.WEBHOOK)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.WEBHOOK, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_WEBHOOK, required = true) + public Builder setEventDestination(EventDestinationConfiguration eventDestination) { + this.eventDestination = OptionalValue.of(eventDestination); + return this; + } + + public EventDestinationCallBehavior build() { + return new EventDestinationCallBehaviorImpl(type, eventDestination); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfiguration.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfiguration.java new file mode 100644 index 000000000..9d65e2f93 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfiguration.java @@ -0,0 +1,68 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** Event destination configuration */ +@JsonDeserialize(builder = EventDestinationConfigurationImpl.Builder.class) +public interface EventDestinationConfiguration { + + /** + * Event destination URL + * + *

Field is required + * + * @return url + */ + String getUrl(); + + /** + * Fallback event destination URL used when the primary URL fails. + * + *

A failed request is re-sent to this URL immediately. After repeated consecutive failures of + * the primary URL, requests are sent only here until the primary URL recovers. + * + *

See Timeouts and failover in the Webhooks section for the + * authoritative algorithm. + * + * @return fallbackUrl + */ + String getFallbackUrl(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new EventDestinationConfigurationImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param url see getter + * @return Current builder + * @see #getUrl + */ + Builder setUrl(String url); + + /** + * see getter + * + * @param fallbackUrl see getter + * @return Current builder + * @see #getFallbackUrl + */ + Builder setFallbackUrl(String fallbackUrl); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + EventDestinationConfiguration build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfigurationImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfigurationImpl.java new file mode 100644 index 000000000..d3968f104 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfigurationImpl.java @@ -0,0 +1,120 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({ + EventDestinationConfigurationImpl.JSON_PROPERTY_URL, + EventDestinationConfigurationImpl.JSON_PROPERTY_FALLBACK_URL +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class EventDestinationConfigurationImpl implements EventDestinationConfiguration { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_URL = "url"; + + private OptionalValue url; + + public static final String JSON_PROPERTY_FALLBACK_URL = "fallbackUrl"; + + private OptionalValue fallbackUrl; + + public EventDestinationConfigurationImpl() {} + + protected EventDestinationConfigurationImpl( + OptionalValue url, OptionalValue fallbackUrl) { + this.url = url; + this.fallbackUrl = fallbackUrl; + } + + @JsonIgnore + public String getUrl() { + return url.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_URL) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue url() { + return url; + } + + @JsonIgnore + public String getFallbackUrl() { + return fallbackUrl.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_FALLBACK_URL) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue fallbackUrl() { + return fallbackUrl; + } + + /** Return true if this EventDestinationConfiguration object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EventDestinationConfigurationImpl eventDestinationConfiguration = + (EventDestinationConfigurationImpl) o; + return Objects.equals(this.url, eventDestinationConfiguration.url) + && Objects.equals(this.fallbackUrl, eventDestinationConfiguration.fallbackUrl); + } + + @Override + public int hashCode() { + return Objects.hash(url, fallbackUrl); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class EventDestinationConfigurationImpl {\n"); + sb.append(" url: ").append(toIndentedString(url)).append("\n"); + sb.append(" fallbackUrl: ").append(toIndentedString(fallbackUrl)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements EventDestinationConfiguration.Builder { + OptionalValue url = OptionalValue.empty(); + OptionalValue fallbackUrl = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_URL, required = true) + public Builder setUrl(String url) { + this.url = OptionalValue.of(url); + return this; + } + + @JsonProperty(JSON_PROPERTY_FALLBACK_URL) + public Builder setFallbackUrl(String fallbackUrl) { + this.fallbackUrl = OptionalValue.of(fallbackUrl); + return this; + } + + public EventDestinationConfiguration build() { + return new EventDestinationConfigurationImpl(url, fallbackUrl); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehavior.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehavior.java new file mode 100644 index 000000000..38d16cf6d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehavior.java @@ -0,0 +1,34 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * No call behavior is configured for the service. Incoming calls will not be handled and outbound + * calls can still be initiated via the API. + */ +@JsonDeserialize(builder = NoneCallBehaviorImpl.Builder.class) +public interface NoneCallBehavior extends CallBehavior { + + /** Default none call behavior, to be used instead of building an empty one */ + NoneCallBehavior NONE_CALL_BEHAVIOR = NoneCallBehavior.builder().build(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new NoneCallBehaviorImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * Create instance + * + * @return The instance build with current builder values + */ + NoneCallBehavior build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehaviorImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehaviorImpl.java new file mode 100644 index 000000000..d48ad3250 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehaviorImpl.java @@ -0,0 +1,121 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import java.util.Arrays; +import java.util.Objects; + +@JsonPropertyOrder({NoneCallBehaviorImpl.JSON_PROPERTY_TYPE}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class NoneCallBehaviorImpl implements NoneCallBehavior, CallBehavior { + private static final long serialVersionUID = 1L; + + /** The type property. Must have the value NONE. */ + public static class TypeEnum extends EnumDynamic { + /** No call behavior is configured. */ + public static final TypeEnum NONE = new TypeEnum("NONE"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(NONE)); + + private TypeEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public NoneCallBehaviorImpl() {} + + protected NoneCallBehaviorImpl(OptionalValue type) { + this.type = type; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + /** Return true if this NoneCallBehavior object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NoneCallBehaviorImpl noneCallBehavior = (NoneCallBehaviorImpl) o; + return Objects.equals(this.type, noneCallBehavior.type); + } + + @Override + public int hashCode() { + return Objects.hash(type); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class NoneCallBehaviorImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements NoneCallBehavior.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.NONE); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.NONE)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.NONE, type)); + } + return this; + } + + public NoneCallBehavior build() { + return new NoneCallBehaviorImpl(type); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehavior.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehavior.java new file mode 100644 index 000000000..cfe49807c --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehavior.java @@ -0,0 +1,50 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; + +/** + * Calls are handled using a predefined static SVAML script. The commands are executed for every + * call on this service, without any backend involvement. + */ +@JsonDeserialize(builder = StaticCallBehaviorImpl.Builder.class) +public interface StaticCallBehavior extends CallBehavior { + + /** + * Predefined SVAML commands executed for every call on this service. + * + *

Field is required + * + * @return static + */ + SvamlInput getStatic(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new StaticCallBehaviorImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param _static see getter + * @return Current builder + * @see #getStatic + */ + Builder setStatic(SvamlInput _static); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + StaticCallBehavior build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehaviorImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehaviorImpl.java new file mode 100644 index 000000000..f6f9cf232 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehaviorImpl.java @@ -0,0 +1,151 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import java.util.Arrays; +import java.util.Objects; + +@JsonPropertyOrder({ + StaticCallBehaviorImpl.JSON_PROPERTY_TYPE, + StaticCallBehaviorImpl.JSON_PROPERTY_STATIC +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class StaticCallBehaviorImpl implements StaticCallBehavior, CallBehavior { + private static final long serialVersionUID = 1L; + + /** The type property. Must have the value STATIC. */ + public static class TypeEnum extends EnumDynamic { + /** Calls are handled using predefined static SVAML commands. */ + public static final TypeEnum STATIC = new TypeEnum("STATIC"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>(TypeEnum.class, TypeEnum::new, Arrays.asList(STATIC)); + + private TypeEnum(String value) { + super(value); + } + + public static java.util.stream.Stream values() { + return ENUM_SUPPORT.values(); + } + + public static TypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(TypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + public static final String JSON_PROPERTY_TYPE = "type"; + + private OptionalValue type; + + public static final String JSON_PROPERTY_STATIC = "static"; + + private OptionalValue _static; + + public StaticCallBehaviorImpl() {} + + protected StaticCallBehaviorImpl( + OptionalValue type, OptionalValue _static) { + this.type = type; + this._static = _static; + } + + @JsonIgnore + public TypeEnum getType() { + return type.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue type() { + return type; + } + + @JsonIgnore + public SvamlInput getStatic() { + return _static.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_STATIC) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue _static() { + return _static; + } + + /** Return true if this StaticCallBehavior object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StaticCallBehaviorImpl staticCallBehavior = (StaticCallBehaviorImpl) o; + return Objects.equals(this.type, staticCallBehavior.type) + && Objects.equals(this._static, staticCallBehavior._static); + } + + @Override + public int hashCode() { + return Objects.hash(type, _static); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class StaticCallBehaviorImpl {\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" _static: ").append(toIndentedString(_static)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements StaticCallBehavior.Builder { + OptionalValue type = OptionalValue.of(TypeEnum.STATIC); + OptionalValue _static = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + Builder setType(TypeEnum type) { + if (!Objects.equals(type, TypeEnum.STATIC)) { + throw new IllegalArgumentException( + String.format("'type' must be '%s' (is '%s')", TypeEnum.STATIC, type)); + } + return this; + } + + @JsonProperty(value = JSON_PROPERTY_STATIC, required = true) + public Builder setStatic(SvamlInput _static) { + this._static = OptionalValue.of(_static); + return this; + } + + public StaticCallBehavior build() { + return new StaticCallBehaviorImpl(type, _static); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/internal/CallBehaviorInternal.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/internal/CallBehaviorInternal.java new file mode 100644 index 000000000..0a29fdd4a --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/internal/CallBehaviorInternal.java @@ -0,0 +1,6 @@ +package com.sinch.sdk.domains.voice.models.v2.services.internal; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(using = CallBehaviorInternalImpl.CallBehaviorInternalImplDeserializer.class) +public interface CallBehaviorInternal {} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/internal/CallBehaviorInternalImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/internal/CallBehaviorInternalImpl.java new file mode 100644 index 000000000..d249f28aa --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/internal/CallBehaviorInternalImpl.java @@ -0,0 +1,178 @@ +package com.sinch.sdk.domains.voice.models.v2.services.internal; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.sinch.sdk.core.models.AbstractOpenApiSchema; +import com.sinch.sdk.core.utils.databind.JSONNavigator; +import com.sinch.sdk.domains.voice.models.v2.services.EventDestinationCallBehaviorImpl; +import com.sinch.sdk.domains.voice.models.v2.services.NoneCallBehaviorImpl; +import com.sinch.sdk.domains.voice.models.v2.services.StaticCallBehaviorImpl; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +@JsonDeserialize(using = CallBehaviorInternalImpl.CallBehaviorInternalImplDeserializer.class) +@JsonSerialize(using = CallBehaviorInternalImpl.CallBehaviorInternalImplSerializer.class) +public class CallBehaviorInternalImpl extends AbstractOpenApiSchema + implements CallBehaviorInternal { + private static final Logger log = Logger.getLogger(CallBehaviorInternalImpl.class.getName()); + + public static final class CallBehaviorInternalImplSerializer + extends StdSerializer { + private static final long serialVersionUID = 1L; + + public CallBehaviorInternalImplSerializer(Class t) { + super(t); + } + + public CallBehaviorInternalImplSerializer() { + this(null); + } + + @Override + public void serialize( + CallBehaviorInternalImpl value, JsonGenerator jgen, SerializerProvider provider) + throws IOException, JsonProcessingException { + jgen.writeObject(value.getActualInstance()); + } + } + + public static final class CallBehaviorInternalImplDeserializer + extends StdDeserializer { + + private static final long serialVersionUID = 1L; + + public CallBehaviorInternalImplDeserializer() { + this(CallBehaviorInternalImpl.class); + } + + public CallBehaviorInternalImplDeserializer(Class vc) { + super(vc); + } + + @Override + public CallBehaviorInternalImpl deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + JsonNode tree = jp.readValueAsTree(); + JsonNode discriminator = tree.get("type"); + String discriminatorValue = null != discriminator ? discriminator.asText() : null; + Class cls = null != discriminatorValue ? mappings.get(discriminatorValue) : null; + if (null == cls) { + throw new IOException( + String.format( + "Failed deserialization for CallBehaviorInternalImpl: unknown 'type' value '%s'." + + " Possible values: %s", + discriminatorValue, mappings.keySet())); + } + Object deserialized = tree.traverse(jp.getCodec()).readValueAs(cls); + CallBehaviorInternalImpl ret = new CallBehaviorInternalImpl(); + ret.setActualInstance(deserialized); + return ret; + } + + /** Handle deserialization of the 'null' value. */ + @Override + public CallBehaviorInternalImpl getNullValue(DeserializationContext ctxt) + throws JsonMappingException { + throw new JsonMappingException(ctxt.getParser(), "CallBehaviorInternalImpl cannot be null"); + } + } + + // store a list of schema names defined in oneOf + public static final Map> schemas = new HashMap<>(); + + // discriminator value -> class + private static final Map> mappings = new HashMap<>(); + + public CallBehaviorInternalImpl() { + super("oneOf", Boolean.FALSE); + } + + static { + schemas.put("NoneCallBehaviorImpl", NoneCallBehaviorImpl.class); + schemas.put("EventDestinationCallBehaviorImpl", EventDestinationCallBehaviorImpl.class); + schemas.put("StaticCallBehaviorImpl", StaticCallBehaviorImpl.class); + JSONNavigator.registerDescendants( + CallBehaviorInternalImpl.class, Collections.unmodifiableMap(schemas)); + // Initialize and register the discriminator mappings. + mappings.put("NONE", NoneCallBehaviorImpl.class); + mappings.put("WEBHOOK", EventDestinationCallBehaviorImpl.class); + mappings.put("STATIC", StaticCallBehaviorImpl.class); + JSONNavigator.registerDiscriminator(CallBehaviorInternalImpl.class, "type", mappings); + } + + @Override + public Map> getSchemas() { + return CallBehaviorInternalImpl.schemas; + } + + /** + * Set the instance that matches the oneOf child schema, check the instance parameter is valid + * against the oneOf child schemas: NoneCallBehaviorImpl, EventDestinationCallBehaviorImpl, + * StaticCallBehaviorImpl + */ + @Override + public void setActualInstance(Object instance) { + for (Class schema : schemas.values()) { + if (JSONNavigator.isInstanceOf(schema, instance, new HashSet>())) { + super.setActualInstance(instance); + return; + } + } + throw new RuntimeException( + "Invalid instance type. Must be NoneCallBehaviorImpl, EventDestinationCallBehaviorImpl," + + " StaticCallBehaviorImpl"); + } + + /** + * Get the actual instance, which can be the following: NoneCallBehaviorImpl, + * EventDestinationCallBehaviorImpl, StaticCallBehaviorImpl + * + * @return The actual instance + */ + @Override + public Object getActualInstance() { + return super.getActualInstance(); + } + + public static class Deserializer + extends StdDeserializer { + + public Deserializer() { + this(null); + } + + public Deserializer(Class vc) { + super(vc); + } + + @Override + public com.sinch.sdk.domains.voice.models.v2.services.CallBehavior deserialize( + JsonParser jp, DeserializationContext ctxt) throws IOException { + + Object deserialized = jp.readValueAs(CallBehaviorInternalImpl.class).getActualInstance(); + if (null == deserialized) { + return null; + } + if (!(deserialized instanceof com.sinch.sdk.domains.voice.models.v2.services.CallBehavior)) { + log.log(Level.SEVERE, "Input data does not match schema ", deserialized); + return null; + } + + return (com.sinch.sdk.domains.voice.models.v2.services.CallBehavior) deserialized; + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequest.java new file mode 100644 index 000000000..f20b9e9c8 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequest.java @@ -0,0 +1,117 @@ +package com.sinch.sdk.domains.voice.models.v2.services.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.services.CallBehavior; + +/** Request body for creating a new voice service. */ +@JsonDeserialize(builder = CreateServiceRequestImpl.Builder.class) +public interface CreateServiceRequest { + + /** + * Voice service name. No leading or trailing whitespaces allowed. + * + *

Field is required + * + * @return name + */ + String getName(); + + /** + * Description of the service + * + * @return description + */ + String getDescription(); + + /** + * Indicates whether this service is set as the default for the project. The default service is + * used when no specific service is specified in API requests. + * + * @return isDefault + */ + Boolean getIsDefault(); + + /** + * Defines how calls are handled for this service + * + * @return callBehavior + */ + CallBehavior getCallBehavior(); + + /** + * Client-generated idempotency key to safely retry requests. The server uses this key to + * recognize retries of the same request. If a request with the same key is received within 10 + * minutes, the server returns the cached response from the original request. Using a random UUID + * (v4) is strongly recommended. + * + *

Sent as the Idempotency-Key header. + * + * @return idempotencyKey + */ + String getIdempotencyKey(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new CreateServiceRequestImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param name see getter + * @return Current builder + * @see #getName + */ + Builder setName(String name); + + /** + * see getter + * + * @param description see getter + * @return Current builder + * @see #getDescription + */ + Builder setDescription(String description); + + /** + * see getter + * + * @param isDefault see getter + * @return Current builder + * @see #getIsDefault + */ + Builder setIsDefault(Boolean isDefault); + + /** + * see getter + * + * @param callBehavior see getter + * @return Current builder + * @see #getCallBehavior + */ + Builder setCallBehavior(CallBehavior callBehavior); + + /** + * see getter + * + * @param idempotencyKey see getter + * @return Current builder + * @see #getIdempotencyKey + */ + Builder setIdempotencyKey(String idempotencyKey); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + CreateServiceRequest build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequestImpl.java new file mode 100644 index 000000000..22c590f5d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequestImpl.java @@ -0,0 +1,199 @@ +package com.sinch.sdk.domains.voice.models.v2.services.request; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.services.CallBehavior; +import java.util.Objects; + +@JsonPropertyOrder({ + CreateServiceRequestImpl.JSON_PROPERTY_NAME, + CreateServiceRequestImpl.JSON_PROPERTY_DESCRIPTION, + CreateServiceRequestImpl.JSON_PROPERTY_IS_DEFAULT, + CreateServiceRequestImpl.JSON_PROPERTY_CALL_BEHAVIOR +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class CreateServiceRequestImpl implements CreateServiceRequest { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_NAME = "name"; + + private OptionalValue name; + + public static final String JSON_PROPERTY_DESCRIPTION = "description"; + + private OptionalValue description; + + public static final String JSON_PROPERTY_IS_DEFAULT = "isDefault"; + + private OptionalValue isDefault; + + public static final String JSON_PROPERTY_CALL_BEHAVIOR = "callBehavior"; + + private OptionalValue callBehavior; + + private OptionalValue idempotencyKey; + + public CreateServiceRequestImpl() {} + + protected CreateServiceRequestImpl( + OptionalValue name, + OptionalValue description, + OptionalValue isDefault, + OptionalValue callBehavior, + OptionalValue idempotencyKey) { + this.name = name; + this.description = description; + this.isDefault = isDefault; + this.callBehavior = callBehavior; + this.idempotencyKey = idempotencyKey; + } + + @JsonIgnore + public String getName() { + return name.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue name() { + return name; + } + + @JsonIgnore + public String getDescription() { + return description.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue description() { + return description; + } + + @JsonIgnore + public Boolean getIsDefault() { + return isDefault.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_IS_DEFAULT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue isDefault() { + return isDefault; + } + + @JsonIgnore + public CallBehavior getCallBehavior() { + return callBehavior.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_BEHAVIOR) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callBehavior() { + return callBehavior; + } + + @JsonIgnore + public String getIdempotencyKey() { + return idempotencyKey.orElse(null); + } + + @JsonIgnore + public OptionalValue idempotencyKey() { + return idempotencyKey; + } + + /** Return true if this CreateServiceRequest object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CreateServiceRequestImpl createServiceRequest = (CreateServiceRequestImpl) o; + return Objects.equals(this.name, createServiceRequest.name) + && Objects.equals(this.description, createServiceRequest.description) + && Objects.equals(this.isDefault, createServiceRequest.isDefault) + && Objects.equals(this.callBehavior, createServiceRequest.callBehavior) + && Objects.equals(this.idempotencyKey, createServiceRequest.idempotencyKey); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, isDefault, callBehavior, idempotencyKey); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CreateServiceRequestImpl {\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" isDefault: ").append(toIndentedString(isDefault)).append("\n"); + sb.append(" callBehavior: ").append(toIndentedString(callBehavior)).append("\n"); + sb.append(" idempotencyKey: ").append(toIndentedString(idempotencyKey)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements CreateServiceRequest.Builder { + OptionalValue name = OptionalValue.empty(); + OptionalValue description = OptionalValue.empty(); + OptionalValue isDefault = OptionalValue.empty(); + OptionalValue callBehavior = OptionalValue.empty(); + OptionalValue idempotencyKey = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_NAME, required = true) + public Builder setName(String name) { + this.name = OptionalValue.of(name); + return this; + } + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + public Builder setDescription(String description) { + this.description = OptionalValue.of(description); + return this; + } + + @JsonProperty(JSON_PROPERTY_IS_DEFAULT) + public Builder setIsDefault(Boolean isDefault) { + this.isDefault = OptionalValue.of(isDefault); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_BEHAVIOR) + public Builder setCallBehavior(CallBehavior callBehavior) { + this.callBehavior = OptionalValue.of(callBehavior); + return this; + } + + @JsonIgnore + public Builder setIdempotencyKey(String idempotencyKey) { + this.idempotencyKey = OptionalValue.of(idempotencyKey); + return this; + } + + public CreateServiceRequest build() { + return new CreateServiceRequestImpl( + name, description, isDefault, callBehavior, idempotencyKey); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponse.java new file mode 100644 index 000000000..4d7085c4b --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponse.java @@ -0,0 +1,177 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.services.CallBehavior; +import java.time.Instant; + +/** + * Full representation of a voice service resource, including its configuration and call behavior. + */ +@JsonDeserialize(builder = ServiceResponseImpl.Builder.class) +public interface ServiceResponse { + + /** + * The ID of the service used. + * + *

Field is required + * + * @return serviceId + */ + String getServiceId(); + + /** + * The ID of the project associated with the call. + * + *

Field is required + * + * @return projectId + */ + String getProjectId(); + + /** + * Timestamp (RFC 3339) indicating when the service was created. + * + *

Field is required + * + * @return createTime + * @readOnly This field is returned by the server and cannot be modified + */ + Instant getCreateTime(); + + /** + * Timestamp (RFC 3339) indicating when the service was last updated. + * + *

Omitted if no updates were performed on this service. + * + * @return updateTime + * @readOnly This field is returned by the server and cannot be modified + */ + Instant getUpdateTime(); + + /** + * Voice service name. No leading or trailing whitespaces allowed. + * + *

Field is required + * + * @return name + */ + String getName(); + + /** + * Description of the service + * + * @return description + */ + String getDescription(); + + /** + * Indicates whether this service is set as the default for the project. The default service is + * used when no specific service is specified in API requests. + * + *

Field is required + * + * @return isDefault + */ + Boolean getIsDefault(); + + /** + * Defines how calls are handled for this service + * + * @return callBehavior + */ + CallBehavior getCallBehavior(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new ServiceResponseImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param serviceId see getter + * @return Current builder + * @see #getServiceId + */ + Builder setServiceId(String serviceId); + + /** + * see getter + * + * @param projectId see getter + * @return Current builder + * @see #getProjectId + */ + Builder setProjectId(String projectId); + + /** + * see getter + * + * @param createTime see getter + * @return Current builder + * @see #getCreateTime + * @readOnly This field is returned by the server and cannot be modified + */ + Builder setCreateTime(Instant createTime); + + /** + * see getter + * + * @param updateTime see getter + * @return Current builder + * @see #getUpdateTime + * @readOnly This field is returned by the server and cannot be modified + */ + Builder setUpdateTime(Instant updateTime); + + /** + * see getter + * + * @param name see getter + * @return Current builder + * @see #getName + */ + Builder setName(String name); + + /** + * see getter + * + * @param description see getter + * @return Current builder + * @see #getDescription + */ + Builder setDescription(String description); + + /** + * see getter + * + * @param isDefault see getter + * @return Current builder + * @see #getIsDefault + */ + Builder setIsDefault(Boolean isDefault); + + /** + * see getter + * + * @param callBehavior see getter + * @return Current builder + * @see #getCallBehavior + */ + Builder setCallBehavior(CallBehavior callBehavior); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + ServiceResponse build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseImpl.java new file mode 100644 index 000000000..4f8a6925d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseImpl.java @@ -0,0 +1,284 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.services.CallBehavior; +import java.time.Instant; +import java.util.Objects; + +@JsonPropertyOrder({ + ServiceResponseImpl.JSON_PROPERTY_SERVICE_ID, + ServiceResponseImpl.JSON_PROPERTY_PROJECT_ID, + ServiceResponseImpl.JSON_PROPERTY_CREATE_TIME, + ServiceResponseImpl.JSON_PROPERTY_UPDATE_TIME, + ServiceResponseImpl.JSON_PROPERTY_NAME, + ServiceResponseImpl.JSON_PROPERTY_DESCRIPTION, + ServiceResponseImpl.JSON_PROPERTY_IS_DEFAULT, + ServiceResponseImpl.JSON_PROPERTY_CALL_BEHAVIOR +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class ServiceResponseImpl implements ServiceResponse { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_SERVICE_ID = "serviceId"; + + private OptionalValue serviceId; + + public static final String JSON_PROPERTY_PROJECT_ID = "projectId"; + + private OptionalValue projectId; + + public static final String JSON_PROPERTY_CREATE_TIME = "createTime"; + + private OptionalValue createTime; + + public static final String JSON_PROPERTY_UPDATE_TIME = "updateTime"; + + private OptionalValue updateTime; + + public static final String JSON_PROPERTY_NAME = "name"; + + private OptionalValue name; + + public static final String JSON_PROPERTY_DESCRIPTION = "description"; + + private OptionalValue description; + + public static final String JSON_PROPERTY_IS_DEFAULT = "isDefault"; + + private OptionalValue isDefault; + + public static final String JSON_PROPERTY_CALL_BEHAVIOR = "callBehavior"; + + private OptionalValue callBehavior; + + public ServiceResponseImpl() {} + + protected ServiceResponseImpl( + OptionalValue serviceId, + OptionalValue projectId, + OptionalValue createTime, + OptionalValue updateTime, + OptionalValue name, + OptionalValue description, + OptionalValue isDefault, + OptionalValue callBehavior) { + this.serviceId = serviceId; + this.projectId = projectId; + this.createTime = createTime; + this.updateTime = updateTime; + this.name = name; + this.description = description; + this.isDefault = isDefault; + this.callBehavior = callBehavior; + } + + @JsonIgnore + public String getServiceId() { + return serviceId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SERVICE_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue serviceId() { + return serviceId; + } + + @JsonIgnore + public String getProjectId() { + return projectId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PROJECT_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue projectId() { + return projectId; + } + + @JsonIgnore + public Instant getCreateTime() { + return createTime.orElse(null); + } + + @JsonIgnore + public OptionalValue createTime() { + return createTime; + } + + @JsonIgnore + public Instant getUpdateTime() { + return updateTime.orElse(null); + } + + @JsonIgnore + public OptionalValue updateTime() { + return updateTime; + } + + @JsonIgnore + public String getName() { + return name.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_NAME) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue name() { + return name; + } + + @JsonIgnore + public String getDescription() { + return description.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue description() { + return description; + } + + @JsonIgnore + public Boolean getIsDefault() { + return isDefault.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_IS_DEFAULT) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue isDefault() { + return isDefault; + } + + @JsonIgnore + public CallBehavior getCallBehavior() { + return callBehavior.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_BEHAVIOR) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callBehavior() { + return callBehavior; + } + + /** Return true if this ServiceResponse object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServiceResponseImpl serviceResponse = (ServiceResponseImpl) o; + return Objects.equals(this.serviceId, serviceResponse.serviceId) + && Objects.equals(this.projectId, serviceResponse.projectId) + && Objects.equals(this.createTime, serviceResponse.createTime) + && Objects.equals(this.updateTime, serviceResponse.updateTime) + && Objects.equals(this.name, serviceResponse.name) + && Objects.equals(this.description, serviceResponse.description) + && Objects.equals(this.isDefault, serviceResponse.isDefault) + && Objects.equals(this.callBehavior, serviceResponse.callBehavior); + } + + @Override + public int hashCode() { + return Objects.hash( + serviceId, projectId, createTime, updateTime, name, description, isDefault, callBehavior); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ServiceResponseImpl {\n"); + sb.append(" serviceId: ").append(toIndentedString(serviceId)).append("\n"); + sb.append(" projectId: ").append(toIndentedString(projectId)).append("\n"); + sb.append(" createTime: ").append(toIndentedString(createTime)).append("\n"); + sb.append(" updateTime: ").append(toIndentedString(updateTime)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" isDefault: ").append(toIndentedString(isDefault)).append("\n"); + sb.append(" callBehavior: ").append(toIndentedString(callBehavior)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements ServiceResponse.Builder { + OptionalValue serviceId = OptionalValue.empty(); + OptionalValue projectId = OptionalValue.empty(); + OptionalValue createTime = OptionalValue.empty(); + OptionalValue updateTime = OptionalValue.empty(); + OptionalValue name = OptionalValue.empty(); + OptionalValue description = OptionalValue.empty(); + OptionalValue isDefault = OptionalValue.empty(); + OptionalValue callBehavior = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_SERVICE_ID, required = true) + public Builder setServiceId(String serviceId) { + this.serviceId = OptionalValue.of(serviceId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_PROJECT_ID, required = true) + public Builder setProjectId(String projectId) { + this.projectId = OptionalValue.of(projectId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CREATE_TIME, required = true) + public Builder setCreateTime(Instant createTime) { + this.createTime = OptionalValue.of(createTime); + return this; + } + + @JsonProperty(JSON_PROPERTY_UPDATE_TIME) + public Builder setUpdateTime(Instant updateTime) { + this.updateTime = OptionalValue.of(updateTime); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_NAME, required = true) + public Builder setName(String name) { + this.name = OptionalValue.of(name); + return this; + } + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + public Builder setDescription(String description) { + this.description = OptionalValue.of(description); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_IS_DEFAULT, required = true) + public Builder setIsDefault(Boolean isDefault) { + this.isDefault = OptionalValue.of(isDefault); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_BEHAVIOR) + public Builder setCallBehavior(CallBehavior callBehavior) { + this.callBehavior = OptionalValue.of(callBehavior); + return this; + } + + public ServiceResponse build() { + return new ServiceResponseImpl( + serviceId, projectId, createTime, updateTime, name, description, isDefault, callBehavior); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehaviorDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehaviorDtoTest.java new file mode 100644 index 000000000..2cb9a4c0e --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationCallBehaviorDtoTest.java @@ -0,0 +1,47 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class EventDestinationCallBehaviorDtoTest extends BaseTest { + + public static final EventDestinationCallBehavior expectedEventDestinationCallBehavior = + EventDestinationCallBehavior.builder() + .setEventDestination( + EventDestinationConfigurationDtoTest.expectedEventDestinationConfiguration) + .build(); + + @GivenTextResource("/domains/voice/v2/services/EventDestinationCallBehaviorDto.json") + String jsonEventDestinationCallBehavior; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedEventDestinationCallBehavior); + + JSONAssert.assertEquals(jsonEventDestinationCallBehavior, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + EventDestinationCallBehavior deserialized = + objectMapper.readValue( + jsonEventDestinationCallBehavior, EventDestinationCallBehavior.class); + + TestHelpers.recursiveEquals(deserialized, expectedEventDestinationCallBehavior); + } + + @Test + void deserializeAsCallBehavior() throws JsonProcessingException { + CallBehavior deserialized = + objectMapper.readValue(jsonEventDestinationCallBehavior, CallBehavior.class); + + TestHelpers.recursiveEquals(deserialized, expectedEventDestinationCallBehavior); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfigurationDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfigurationDtoTest.java new file mode 100644 index 000000000..c1894b2cc --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/EventDestinationConfigurationDtoTest.java @@ -0,0 +1,40 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class EventDestinationConfigurationDtoTest extends BaseTest { + + public static final EventDestinationConfiguration expectedEventDestinationConfiguration = + EventDestinationConfiguration.builder() + .setUrl("https://example.com/webhook") + .setFallbackUrl("https://example.com/fallback") + .build(); + + @GivenTextResource("/domains/voice/v2/services/EventDestinationConfigurationDto.json") + String jsonEventDestinationConfiguration; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = + objectMapper.writeValueAsString(expectedEventDestinationConfiguration); + + JSONAssert.assertEquals(jsonEventDestinationConfiguration, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + EventDestinationConfiguration deserialized = + objectMapper.readValue( + jsonEventDestinationConfiguration, EventDestinationConfiguration.class); + + TestHelpers.recursiveEquals(deserialized, expectedEventDestinationConfiguration); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehaviorDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehaviorDtoTest.java new file mode 100644 index 000000000..3dd84c3dc --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/NoneCallBehaviorDtoTest.java @@ -0,0 +1,42 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class NoneCallBehaviorDtoTest extends BaseTest { + + public static final NoneCallBehavior expectedNoneCallBehavior = + NoneCallBehavior.NONE_CALL_BEHAVIOR; + + @GivenTextResource("/domains/voice/v2/services/NoneCallBehaviorDto.json") + String jsonNoneCallBehavior; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedNoneCallBehavior); + + JSONAssert.assertEquals(jsonNoneCallBehavior, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + NoneCallBehavior deserialized = + objectMapper.readValue(jsonNoneCallBehavior, NoneCallBehavior.class); + + TestHelpers.recursiveEquals(deserialized, expectedNoneCallBehavior); + } + + @Test + void deserializeAsCallBehavior() throws JsonProcessingException { + CallBehavior deserialized = objectMapper.readValue(jsonNoneCallBehavior, CallBehavior.class); + + TestHelpers.recursiveEquals(deserialized, expectedNoneCallBehavior); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehaviorDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehaviorDtoTest.java new file mode 100644 index 000000000..bbf8dee29 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/StaticCallBehaviorDtoTest.java @@ -0,0 +1,43 @@ +package com.sinch.sdk.domains.voice.models.v2.services; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInputDtoTest; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class StaticCallBehaviorDtoTest extends BaseTest { + + public static final StaticCallBehavior expectedStaticCallBehavior = + StaticCallBehavior.builder().setStatic(SvamlInputDtoTest.expectedSvamlInput).build(); + + @GivenTextResource("/domains/voice/v2/services/StaticCallBehaviorDto.json") + String jsonStaticCallBehavior; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedStaticCallBehavior); + + JSONAssert.assertEquals(jsonStaticCallBehavior, serializedString, true); + } + + @Test + void deserialize() throws JsonProcessingException { + StaticCallBehavior deserialized = + objectMapper.readValue(jsonStaticCallBehavior, StaticCallBehavior.class); + + TestHelpers.recursiveEquals(deserialized, expectedStaticCallBehavior); + } + + @Test + void deserializeAsCallBehavior() throws JsonProcessingException { + CallBehavior deserialized = objectMapper.readValue(jsonStaticCallBehavior, CallBehavior.class); + + TestHelpers.recursiveEquals(deserialized, expectedStaticCallBehavior); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequestDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequestDtoTest.java new file mode 100644 index 000000000..8905ff802 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequestDtoTest.java @@ -0,0 +1,33 @@ +package com.sinch.sdk.domains.voice.models.v2.services.request; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.services.EventDestinationCallBehaviorDtoTest; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class CreateServiceRequestDtoTest extends BaseTest { + + public static final CreateServiceRequest expectedCreateServiceRequest = + CreateServiceRequest.builder() + .setName("Example service") + .setDescription("Service with webhooks") + .setIsDefault(true) + .setCallBehavior(EventDestinationCallBehaviorDtoTest.expectedEventDestinationCallBehavior) + .setIdempotencyKey("3f9c2a7e-8b1d-4e6f-9a2c-5d7e1b4f8c30") + .build(); + + @GivenTextResource("/domains/voice/v2/services/request/CreateServiceRequestDto.json") + String jsonCreateServiceRequest; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedCreateServiceRequest); + + JSONAssert.assertEquals(jsonCreateServiceRequest, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseDtoTest.java new file mode 100644 index 000000000..464aa19b5 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseDtoTest.java @@ -0,0 +1,57 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.services.EventDestinationCallBehaviorDtoTest; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class ServiceResponseDtoTest extends BaseTest { + + public static final ServiceResponse expectedServiceResponse = + ServiceResponse.builder() + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setCreateTime(Instant.parse("2025-01-01T00:00:00Z")) + .setUpdateTime(Instant.parse("2025-02-15T10:30:00Z")) + .setName("Example service") + .setDescription("Service with webhooks") + .setIsDefault(false) + .setCallBehavior(EventDestinationCallBehaviorDtoTest.expectedEventDestinationCallBehavior) + .build(); + + public static final ServiceResponse expectedServiceResponseMinimal = + ServiceResponse.builder() + .setServiceId("7f235289-d3ae-57b6-a54d-6d3bf655bbdf") + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setCreateTime(Instant.parse("2025-02-15T10:30:00Z")) + .setName("Secondary Service") + .setIsDefault(false) + .build(); + + @GivenTextResource("/domains/voice/v2/services/response/ServiceResponseDto.json") + String jsonServiceResponse; + + @GivenTextResource("/domains/voice/v2/services/response/ServiceResponseMinimalDto.json") + String jsonServiceResponseMinimal; + + @Test + void deserialize() throws JsonProcessingException { + ServiceResponse deserialized = + objectMapper.readValue(jsonServiceResponse, ServiceResponse.class); + + TestHelpers.recursiveEquals(deserialized, expectedServiceResponse); + } + + @Test + void deserializeMinimalDto() throws JsonProcessingException { + ServiceResponse deserialized = + objectMapper.readValue(jsonServiceResponseMinimal, ServiceResponse.class); + + TestHelpers.recursiveEquals(deserialized, expectedServiceResponseMinimal); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/EventDestinationCallBehaviorDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/EventDestinationCallBehaviorDto.json new file mode 100644 index 000000000..1a36b7937 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/EventDestinationCallBehaviorDto.json @@ -0,0 +1,7 @@ +{ + "type": "WEBHOOK", + "webhook": { + "url": "https://example.com/webhook", + "fallbackUrl": "https://example.com/fallback" + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/EventDestinationConfigurationDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/EventDestinationConfigurationDto.json new file mode 100644 index 000000000..817b70728 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/EventDestinationConfigurationDto.json @@ -0,0 +1,4 @@ +{ + "url": "https://example.com/webhook", + "fallbackUrl": "https://example.com/fallback" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/NoneCallBehaviorDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/NoneCallBehaviorDto.json new file mode 100644 index 000000000..462a309f6 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/NoneCallBehaviorDto.json @@ -0,0 +1,3 @@ +{ + "type": "NONE" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/StaticCallBehaviorDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/StaticCallBehaviorDto.json new file mode 100644 index 000000000..8322599a6 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/StaticCallBehaviorDto.json @@ -0,0 +1,20 @@ +{ + "type": "STATIC", + "static": { + "commands": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "callName": "incoming", + "events": { + "onHangup": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/request/CreateServiceRequestDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/request/CreateServiceRequestDto.json new file mode 100644 index 000000000..ad0a0e816 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/request/CreateServiceRequestDto.json @@ -0,0 +1,12 @@ +{ + "name": "Example service", + "description": "Service with webhooks", + "isDefault": true, + "callBehavior": { + "type": "WEBHOOK", + "webhook": { + "url": "https://example.com/webhook", + "fallbackUrl": "https://example.com/fallback" + } + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseDto.json new file mode 100644 index 000000000..867b26afd --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseDto.json @@ -0,0 +1,16 @@ +{ + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "createTime": "2025-01-01T00:00:00Z", + "updateTime": "2025-02-15T10:30:00Z", + "name": "Example service", + "description": "Service with webhooks", + "isDefault": false, + "callBehavior": { + "type": "WEBHOOK", + "webhook": { + "url": "https://example.com/webhook", + "fallbackUrl": "https://example.com/fallback" + } + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseMinimalDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseMinimalDto.json new file mode 100644 index 000000000..913100347 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseMinimalDto.json @@ -0,0 +1,7 @@ +{ + "serviceId": "7f235289-d3ae-57b6-a54d-6d3bf655bbdf", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "createTime": "2025-02-15T10:30:00Z", + "name": "Secondary Service", + "isDefault": false +} From 33e4aa4dcc102966e3ac237bc1d839aafba5c917 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 14:03:01 +0200 Subject: [PATCH 32/50] Add Voice v2 services.get endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/ServicesServiceTest.java | 38 +++++++++++ .../e2e/domains/voice/v2/ServicesSteps.java | 24 +++++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 1 + .../snippets/src/main/java/voice/README.md | 1 + .../src/main/java/voice/v2/services/Get.java | 47 ++++++++++++++ .../domains/voice/api/v2/ServicesService.java | 12 ++++ .../api/v2/adapters/ServicesServiceImpl.java | 64 +++++++++++++++++++ 8 files changed, 189 insertions(+) create mode 100644 examples/snippets/src/main/java/voice/v2/services/Get.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fac56318..ebf3ec525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Batches API: `stop` operation to stop processing a batch of calls - **[feature]** Voice V2 Sessions API: `get` operation to retrieve a session and its calls - **[feature]** Voice V2 Services API: `create` operation to create a voice service +- **[feature]** Voice V2 Services API: `get` operation to retrieve a voice service by its ID - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet @@ -42,6 +43,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/batches/Stop` snippet - **[doc]** New `voice/v2/sessions/Get` snippet - **[doc]** New `voice/v2/services/Create` snippet +- **[doc]** New `voice/v2/services/Get` snippet - **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java index a31932f6c..0fe63212d 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java @@ -147,4 +147,42 @@ void createMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + + @Test + void get() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/services/" + + URLPathUtils.encodePathSegment("6e124178-c29d-46a5-943c-5c2ae544aade"), + HttpMethod.GET, + Collections.emptyList(), + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + ServiceResponse response = service.get("6e124178-c29d-46a5-943c-5c2ae544aade"); + + TestHelpers.recursiveEquals(response, ServiceResponseDtoTest.expectedServiceResponse); + } + + @Test + void getMissingServiceIdThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.get(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java index adea55b09..d4a96de49 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java @@ -2,6 +2,7 @@ import com.sinch.sdk.core.TestHelpers; import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.domains.voice.models.v2.services.NoneCallBehavior; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; import com.sinch.sdk.e2e.Config; @@ -15,6 +16,7 @@ public class ServicesSteps { ServicesService service; ServiceResponse createResponse; + ServiceResponse getResponse; @Given("^the Voice-V2 service \"Services\" is available$") public void serviceAvailable() { @@ -47,4 +49,26 @@ public void createResult() { TestHelpers.recursiveEquals(createResponse, expected); } + + @When("^I send a request to get Voice-V2 service details$") + public void get() { + getResponse = service.get("3c4d5e6f-7a8b-4901-c234-d5e6f7a8b901"); + } + + @Then("^the response contains the Voice-V2 service details$") + public void getResult() { + ServiceResponse expected = + ServiceResponse.builder() + .setServiceId("3c4d5e6f-7a8b-4901-c234-d5e6f7a8b901") + .setProjectId("c3d4e5f6-a7b8-4901-c234-d5e6f7a8b901") + .setCreateTime(Instant.parse("2026-09-16T10:47:36Z")) + .setUpdateTime(Instant.parse("2026-09-16T10:47:36Z")) + .setName("Example service 2") + .setDescription("") + .setIsDefault(false) + .setCallBehavior(NoneCallBehavior.NONE_CALL_BEHAVIOR) + .build(); + + TestHelpers.recursiveEquals(getResponse, expected); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 7192cb0d0..116a32e9f 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -37,6 +37,7 @@ + "|^\\[InteractByCallId\\] interact with an ongoing call by call ID$" + "|^\\[InteractByCallName\\] interact with an ongoing call by call name$" + "|^\\[Create\\] create a Voice-V2 service$" + + "|^\\[Get\\] get Voice-V2 service details$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 2667c1f67..3cbb26af6 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -38,3 +38,4 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/sessions/Get](./v2/sessions/Get.java) - Services - [voice/v2/services/Create](./v2/services/Create.java) + - [voice/v2/services/Get](./v2/services/Get.java) diff --git a/examples/snippets/src/main/java/voice/v2/services/Get.java b/examples/snippets/src/main/java/voice/v2/services/Get.java new file mode 100644 index 000000000..197c772a9 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/services/Get.java @@ -0,0 +1,47 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.services; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class Get { + + private static final Logger LOGGER = Logger.getLogger(Get.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the service to retrieve + String serviceId = "SERVICE_ID"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + ServicesService servicesService = client.voice().v2().services(); + + LOGGER.info(String.format("Get voice service with ID '%s'", serviceId)); + + ServiceResponse response = servicesService.get(serviceId); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java index 817d52dfb..73235c854 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java @@ -18,4 +18,16 @@ public interface ServicesService { * @since 2.3 */ ServiceResponse create(CreateServiceRequest createServiceRequest) throws ApiException; + + /** + * Retrieve a voice service by ID + * + *

Retrieve the full details of a specific voice service by its serviceId. + * + * @param serviceId The ID of the service. (required) + * @return ServiceResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + ServiceResponse get(String serviceId) throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java index 5be13cae8..1c7cc7208 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java @@ -111,4 +111,68 @@ private HttpRequest createRequestBuilder(CreateServiceRequest createServiceReque localVarContentTypes, localVarAuthNames); } + + @Override + public ServiceResponse get(String serviceId) throws ApiException { + + LOGGER.finest("[get]" + " " + "serviceId: " + serviceId); + + HttpRequest httpRequest = getRequestBuilder(serviceId); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest getRequestBuilder(String serviceId) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling get"); + } + // verify the required parameter 'serviceId' is set + if (serviceId == null) { + throw new ApiException(400, "Missing the required parameter 'serviceId' when calling get"); + } + + String localVarPath = + "/v2/projects/{projectId}/services/{serviceId}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "serviceId" + "\\}", URLPathUtils.encodePathSegment(serviceId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.GET, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } From fb91faa3567ef078303c830df68a78744ea8f1cb Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 16:29:41 +0200 Subject: [PATCH 33/50] Add Voice v2 services.list endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/ServicesServiceTest.java | 133 +++++++++ .../e2e/domains/voice/v2/ServicesSteps.java | 49 ++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 1 + .../snippets/src/main/java/voice/README.md | 1 + .../src/main/java/voice/v2/services/List.java | 48 ++++ .../domains/voice/api/v2/ServicesService.java | 39 +++ .../api/v2/adapters/CallsServiceImpl.java | 38 +-- .../v2/adapters/PaginationLinksHelper.java | 63 +++++ .../api/v2/adapters/ServicesServiceImpl.java | 108 ++++++++ .../request/ListServicesQueryParameters.java | 106 ++++++++ .../ListServicesQueryParametersImpl.java | 126 +++++++++ .../response/ServiceShortResponse.java | 164 ++++++++++++ .../response/ServiceShortResponseImpl.java | 252 ++++++++++++++++++ .../response/ServicesListResponse.java | 48 ++++ .../ServicesListResponseInternal.java | 86 ++++++ .../ServicesListResponseInternalImpl.java | 152 +++++++++++ .../response/ServiceShortResponseDtoTest.java | 55 ++++ .../ServicesListResponseInternalDtoTest.java | 76 ++++++ .../response/ServiceShortResponseDto.json | 9 + .../ServiceShortResponseMinimalDto.json | 7 + .../ServicesListResponseInternalDto.json | 30 +++ ...rvicesListResponseInternalLastPageDto.json | 21 ++ 23 files changed, 1577 insertions(+), 37 deletions(-) create mode 100644 examples/snippets/src/main/java/voice/v2/services/List.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/PaginationLinksHelper.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/ListServicesQueryParameters.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/ListServicesQueryParametersImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponseImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServicesListResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternal.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternalImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponseDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternalDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceShortResponseDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceShortResponseMinimalDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/response/internal/ServicesListResponseInternalDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/response/internal/ServicesListResponseInternalLastPageDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf3ec525..d5b0f6789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Sessions API: `get` operation to retrieve a session and its calls - **[feature]** Voice V2 Services API: `create` operation to create a voice service - **[feature]** Voice V2 Services API: `get` operation to retrieve a voice service by its ID +- **[feature]** Voice V2 Services API: `list` operation to list and filter the voice services of a project - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet @@ -44,6 +45,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/sessions/Get` snippet - **[doc]** New `voice/v2/services/Create` snippet - **[doc]** New `voice/v2/services/Get` snippet +- **[doc]** New `voice/v2/services/List` snippet - **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java index 0fe63212d..262afe34c 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java @@ -17,17 +17,24 @@ import com.sinch.sdk.core.http.HttpRequest; import com.sinch.sdk.core.http.HttpRequestTest.HttpRequestMatcher; import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.URLParameter; import com.sinch.sdk.core.http.URLPathUtils; import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.domains.voice.api.v2.ServicesService; import com.sinch.sdk.domains.voice.models.v2.services.NoneCallBehavior; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.services.request.ListServicesQueryParameters; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponseDtoTest; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceShortResponse; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServicesListResponse; +import com.sinch.sdk.domains.voice.models.v2.services.response.internal.ServicesListResponseInternalDtoTest; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -42,6 +49,7 @@ public class ServicesServiceTest extends BaseTest { @Mock Map authManagers; static final String PROJECT_ID = "test_project_id"; + static final String CONFIGURED_SERVER_URL = "https://configured.server.com"; static final Collection AUTH_NAMES = Arrays.asList("BasicAuth", "SinchOAuth2"); static final Collection ACCEPTS = Arrays.asList(HttpContentType.APPLICATION_JSON, "application/problem+json"); @@ -54,6 +62,14 @@ public class ServicesServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/services/response/ServiceResponseDto.json") String jsonServiceResponseDto; + @GivenTextResource( + "/domains/voice/v2/services/response/internal/ServicesListResponseInternalDto.json") + String jsonServicesListResponseDto; + + @GivenTextResource( + "/domains/voice/v2/services/response/internal/ServicesListResponseInternalLastPageDto.json") + String jsonServicesListLastPageResponseDto; + @BeforeEach public void initMocks() { service = @@ -185,4 +201,121 @@ void getMissingServiceIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + + @Test + void list() throws ApiException { + + List urlParameters = + Arrays.asList( + new URLParameter("filter", "Service", URLParameter.form, true), + new URLParameter("isDefault", false, URLParameter.form, true), + new URLParameter("pageSize", 2, URLParameter.form, true), + new URLParameter("page", 1, URLParameter.form, true)); + + mockListPage(urlParameters, jsonServicesListResponseDto); + mockNextListPage(jsonServicesListLastPageResponseDto); + + ListServicesQueryParameters queryParameters = + ListServicesQueryParameters.builder() + .setFilter("Service") + .setIsDefault(false) + .setPageSize(2) + .setPage(1) + .build(); + + ServicesListResponse response = service.list(queryParameters); + + TestHelpers.recursiveEquals( + new ArrayList<>(response.getContent()), + new ArrayList<>( + ServicesListResponseInternalDtoTest.expectedServicesListResponse.getServices())); + Assertions.assertTrue(response.hasNextPage()); + + ServicesListResponse nextResponse = response.nextPage(); + + TestHelpers.recursiveEquals( + new ArrayList<>(nextResponse.getContent()), + new ArrayList<>( + ServicesListResponseInternalDtoTest.expectedServicesListLastPageResponse + .getServices())); + Assertions.assertFalse(nextResponse.hasNextPage()); + } + + @Test + void listIteratesOverAllPages() throws ApiException { + + mockListPage(Collections.emptyList(), jsonServicesListResponseDto); + mockNextListPage(jsonServicesListLastPageResponseDto); + + ServicesListResponse response = service.list(); + + List services = new ArrayList<>(); + response.iterator().forEachRemaining(services::add); + + List expected = + new ArrayList<>( + ServicesListResponseInternalDtoTest.expectedServicesListResponse.getServices()); + expected.addAll( + ServicesListResponseInternalDtoTest.expectedServicesListLastPageResponse.getServices()); + TestHelpers.recursiveEquals(services, expected); + } + + @Test + void listMissingProjectIdThrows() { + + ServicesService serviceWithoutProjectId = + new ServicesServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows(ApiException.class, () -> serviceWithoutProjectId.list()); + + Assertions.assertEquals(400, thrown.getCode()); + } + + // The "next" link served by the fixture targets https://voice.api.sinch.com: its path and query + // are expected to be sent to the configured server instead + private void mockNextListPage(String jsonResponse) { + when(serverConfiguration.getUrl()).thenReturn(CONFIGURED_SERVER_URL); + + HttpRequest httpRequest = + new HttpRequest( + CONFIGURED_SERVER_URL + + "/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=2&pageSize=2", + HttpMethod.GET, + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonResponse.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + } + + private void mockListPage(List urlParameters, String jsonResponse) { + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/services", + HttpMethod.GET, + urlParameters, + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonResponse.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java index d4a96de49..512fd49ea 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java @@ -4,12 +4,15 @@ import com.sinch.sdk.domains.voice.api.v2.ServicesService; import com.sinch.sdk.domains.voice.models.v2.services.NoneCallBehavior; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.request.ListServicesQueryParameters; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServicesListResponse; import com.sinch.sdk.e2e.Config; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import java.time.Instant; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Assertions; public class ServicesSteps { @@ -17,6 +20,7 @@ public class ServicesSteps { ServicesService service; ServiceResponse createResponse; ServiceResponse getResponse; + ServicesListResponse listOnePageResponse; @Given("^the Voice-V2 service \"Services\" is available$") public void serviceAvailable() { @@ -71,4 +75,49 @@ public void getResult() { TestHelpers.recursiveEquals(getResponse, expected); } + + @When("^I send a request to list Voice-V2 services$") + public void listOnePage() { + listOnePageResponse = + service.list(ListServicesQueryParameters.builder().setPageSize(2).build()); + } + + @When("^I send a request to list all the Voice-V2 services$") + public void listAll() { + listOnePageResponse = + service.list(ListServicesQueryParameters.builder().setPageSize(2).build()); + } + + @When("^I iterate manually over the Voice-V2 services pages$") + public void listAllByPage() { + listOnePageResponse = + service.list(ListServicesQueryParameters.builder().setPageSize(2).build()); + } + + @Then("the response contains \"{int}\" Voice-V2 services") + public void onePageResult(int expected) { + + Assertions.assertEquals(expected, listOnePageResponse.getContent().size()); + } + + @Then("the services list contains \"{int}\" Voice-V2 services") + public void listAllResult(int expected) { + + AtomicInteger count = new AtomicInteger(); + listOnePageResponse.iterator().forEachRemaining(_unused -> count.getAndIncrement()); + + Assertions.assertEquals(expected, count.get()); + } + + @Then("the services iteration result contains the data from \"{int}\" Voice-V2 service pages") + public void listAllByPageResult(int expected) { + + int count = listOnePageResponse.getContent().isEmpty() ? 0 : 1; + ServicesListResponse currentPage = listOnePageResponse; + while (currentPage.hasNextPage()) { + count++; + currentPage = currentPage.nextPage(); + } + Assertions.assertEquals(expected, count); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 116a32e9f..07681c3fd 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -38,6 +38,7 @@ + "|^\\[InteractByCallName\\] interact with an ongoing call by call name$" + "|^\\[Create\\] create a Voice-V2 service$" + "|^\\[Get\\] get Voice-V2 service details$" + + "|^\\[List\\] list (a page of|all the) Voice-V2 services.*$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 3cbb26af6..56edb3a25 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -39,3 +39,4 @@ See main [README.md](../../../../README.md) for how to execute snippets - Services - [voice/v2/services/Create](./v2/services/Create.java) - [voice/v2/services/Get](./v2/services/Get.java) + - [voice/v2/services/List](./v2/services/List.java) diff --git a/examples/snippets/src/main/java/voice/v2/services/List.java b/examples/snippets/src/main/java/voice/v2/services/List.java new file mode 100644 index 000000000..bfed341a5 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/services/List.java @@ -0,0 +1,48 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.services; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServicesListResponse; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class List { + + private static final Logger LOGGER = Logger.getLogger(List.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + ServicesService servicesService = client.voice().v2().services(); + + LOGGER.info("Listing voice services"); + + ServicesListResponse response = servicesService.list(); + + LOGGER.info("Response"); + + response + .iterator() + .forEachRemaining(f -> LOGGER.info(String.format("%s: %s", f.getServiceId(), f))); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java index 73235c854..62ac103b3 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java @@ -2,7 +2,9 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.request.ListServicesQueryParameters; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServicesListResponse; /** Services Service */ public interface ServicesService { @@ -30,4 +32,41 @@ public interface ServicesService { * @since 2.3 */ ServiceResponse get(String serviceId) throws ApiException; + + /** + * List all services of a project + * + *

Retrieve a list of voice services in the specified project. + * + *

Optionally: + * + *

    + *
  • Filter services by partial match on name or description (filter) + *
  • Return only the default service (isDefault=true) + *
+ * + * @return ServicesListResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + ServicesListResponse list() throws ApiException; + + /** + * List all services of a project + * + *

Retrieve a list of voice services in the specified project. + * + *

Optionally: + * + *

    + *
  • Filter services by partial match on name or description (filter) + *
  • Return only the default service (isDefault=true) + *
+ * + * @param queryParameter (optional) + * @return ServicesListResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + ServicesListResponse list(ListServicesQueryParameters queryParameter) throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java index ce21ba9d5..d9aa4dcd5 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java @@ -17,9 +17,7 @@ import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.core.models.pagination.Page; import com.sinch.sdk.core.models.pagination.PageNavigator; -import com.sinch.sdk.core.utils.StringUtil; import com.sinch.sdk.domains.voice.models.v2.Call; -import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; import com.sinch.sdk.domains.voice.models.v2.calls.request.CallPatchRequest; import com.sinch.sdk.domains.voice.models.v2.calls.request.ListCallsQueryParameters; import com.sinch.sdk.domains.voice.models.v2.calls.request.StartCallQueryParameters; @@ -27,8 +25,6 @@ import com.sinch.sdk.domains.voice.models.v2.calls.response.CallsListResponse; import com.sinch.sdk.domains.voice.models.v2.calls.response.StartCallResponse; import com.sinch.sdk.domains.voice.models.v2.calls.response.internal.CallsListResponseInternal; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -240,10 +236,8 @@ private CallsListResponse _fetchListPage(HttpRequest httpRequest) throws ApiExce CallsListResponseInternal deserialized = mapper.deserialize(response, new TypeReference() {}); - PaginationLinks links = deserialized.getLinks(); - String nextLink = null != links ? links.getNext() : null; final HttpRequest nextHttpRequest = - StringUtil.isEmpty(nextLink) ? null : nextPageRequestBuilder(nextLink); + PaginationLinksHelper.nextPageRequest(this.serverConfiguration, deserialized.getLinks()); return new CallsListResponse( () -> _fetchListPage(nextHttpRequest), @@ -258,36 +252,6 @@ private CallsListResponse _fetchListPage(HttpRequest httpRequest) throws ApiExce mapper.deserialize(response, new TypeReference>() {})); } - // The API serves "next" as an absolute URL: its path and query are followed as they are, but - // against the configured server, so that credentials are never sent to a host taken from a - // response - private HttpRequest nextPageRequestBuilder(String nextLink) throws ApiException { - URI next; - URI server; - try { - next = new URI(nextLink); - server = new URI(this.serverConfiguration.getUrl()); - } catch (URISyntaxException e) { - throw new ApiException("Invalid next page link: " + nextLink, e); - } - - String fullUrl = - server.getScheme() - + "://" - + server.getRawAuthority() - + next.getRawPath() - + (null != next.getRawQuery() ? "?" + next.getRawQuery() : ""); - - return new HttpRequest( - fullUrl, - HttpMethod.GET, - null, - new HashMap<>(), - Arrays.asList("application/json", "application/problem+json"), - Arrays.asList(), - Arrays.asList("BasicAuth", "SinchOAuth2")); - } - private HttpRequest listRequestBuilder(ListCallsQueryParameters queryParameter) throws ApiException { // verify the required parameter 'this.projectId' is set diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/PaginationLinksHelper.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/PaginationLinksHelper.java new file mode 100644 index 000000000..7ef8227bb --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/PaginationLinksHelper.java @@ -0,0 +1,63 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.core.utils.StringUtil; +import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashMap; + +/** Follows the links.next URL of Voice v2 list responses */ +final class PaginationLinksHelper { + + private PaginationLinksHelper() {} + + /** + * Build the request for the page after the current one + * + * @param serverConfiguration Server the request is sent to + * @param links Links of the current page + * @return The request for the next page, or null on the last page + * @throws ApiException if the next link is not a valid URI + */ + static HttpRequest nextPageRequest(ServerConfiguration serverConfiguration, PaginationLinks links) + throws ApiException { + + String nextLink = null != links ? links.getNext() : null; + if (StringUtil.isEmpty(nextLink)) { + return null; + } + + // The API serves "next" as an absolute URL: its path and query are followed as they are, but + // against the configured server, so that credentials are never sent to a host taken from a + // response + URI next; + URI server; + try { + next = new URI(nextLink); + server = new URI(serverConfiguration.getUrl()); + } catch (URISyntaxException e) { + throw new ApiException("Invalid next page link: " + nextLink, e); + } + + String fullUrl = + server.getScheme() + + "://" + + server.getRawAuthority() + + next.getRawPath() + + (null != next.getRawQuery() ? "?" + next.getRawQuery() : ""); + + return new HttpRequest( + fullUrl, + HttpMethod.GET, + null, + new HashMap<>(), + Arrays.asList("application/json", "application/problem+json"), + Arrays.asList(), + Arrays.asList("BasicAuth", "SinchOAuth2")); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java index 1c7cc7208..e18fa3df9 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java @@ -11,10 +11,16 @@ import com.sinch.sdk.core.http.HttpResponse; import com.sinch.sdk.core.http.HttpStatus; import com.sinch.sdk.core.http.URLParameter; +import com.sinch.sdk.core.http.URLParameterUtils; import com.sinch.sdk.core.http.URLPathUtils; import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.core.models.pagination.Page; +import com.sinch.sdk.core.models.pagination.PageNavigator; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.request.ListServicesQueryParameters; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServicesListResponse; +import com.sinch.sdk.domains.voice.models.v2.services.response.internal.ServicesListResponseInternal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -175,4 +181,106 @@ private HttpRequest getRequestBuilder(String serviceId) throws ApiException { localVarContentTypes, localVarAuthNames); } + + @Override + public ServicesListResponse list() throws ApiException { + + return list((ListServicesQueryParameters) null); + } + + @Override + public ServicesListResponse list(ListServicesQueryParameters queryParameter) throws ApiException { + + LOGGER.finest("[list]" + " " + "queryParameter: " + queryParameter); + + HttpRequest httpRequest = listRequestBuilder(queryParameter); + return _fetchListPage(httpRequest); + } + + private ServicesListResponse _fetchListPage(HttpRequest httpRequest) throws ApiException { + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + + ServicesListResponseInternal deserialized = + mapper.deserialize(response, new TypeReference() {}); + + final HttpRequest nextHttpRequest = + PaginationLinksHelper.nextPageRequest(this.serverConfiguration, deserialized.getLinks()); + + return new ServicesListResponse( + () -> _fetchListPage(nextHttpRequest), + new Page<>(deserialized.getServices(), new PageNavigator<>(nextHttpRequest))); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest listRequestBuilder(ListServicesQueryParameters queryParameter) + throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling list"); + } + + String localVarPath = + "/v2/projects/{projectId}/services" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())); + + List localVarQueryParams = new ArrayList<>(); + if (null != queryParameter) { + + URLParameterUtils.addQueryParam( + queryParameter.getFilter(), "filter", URLParameter.form, null, localVarQueryParams, true); + + URLParameterUtils.addQueryParam( + queryParameter.getIsDefault(), + "isDefault", + URLParameter.form, + null, + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getPageSize(), + "pageSize", + URLParameter.form, + null, + localVarQueryParams, + true); + + URLParameterUtils.addQueryParam( + queryParameter.getPage(), "page", URLParameter.form, null, localVarQueryParams, true); + } + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.GET, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/ListServicesQueryParameters.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/ListServicesQueryParameters.java new file mode 100644 index 000000000..51b58788d --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/ListServicesQueryParameters.java @@ -0,0 +1,106 @@ +package com.sinch.sdk.domains.voice.models.v2.services.request; + +import com.sinch.sdk.core.models.OptionalValue; + +/** + * Query parameters for listing services + * + * @since 2.3 + */ +public interface ListServicesQueryParameters { + + /** + * Filter services by name or description. Returns all services where either the name or + * description contains the specified value (case-insensitive partial match). + * + * @return filter + */ + OptionalValue getFilter(); + + /** + * Return the default service only. + * + * @return isDefault + */ + OptionalValue getIsDefault(); + + /** + * Number of items to be returned on each page. minimum: 1 maximum: 100 + * + * @return pageSize + */ + OptionalValue getPageSize(); + + /** + * Page number (1-based) minimum: 1 + * + * @return page + */ + OptionalValue getPage(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new ListServicesQueryParametersImpl.Builder(); + } + + /** + * Getting builder from existing instance + * + * @param parameters Instance to copy the values from + * @return New Builder instance + */ + static Builder builder(ListServicesQueryParameters parameters) { + return new ListServicesQueryParametersImpl.Builder(parameters); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param filter see getter + * @return Current builder + * @see #getFilter + */ + Builder setFilter(String filter); + + /** + * see getter + * + * @param isDefault see getter + * @return Current builder + * @see #getIsDefault + */ + Builder setIsDefault(Boolean isDefault); + + /** + * see getter + * + * @param pageSize see getter + * @return Current builder + * @see #getPageSize + */ + Builder setPageSize(Integer pageSize); + + /** + * see getter + * + * @param page see getter + * @return Current builder + * @see #getPage + */ + Builder setPage(Integer page); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + ListServicesQueryParameters build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/ListServicesQueryParametersImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/ListServicesQueryParametersImpl.java new file mode 100644 index 000000000..276910c6a --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/ListServicesQueryParametersImpl.java @@ -0,0 +1,126 @@ +package com.sinch.sdk.domains.voice.models.v2.services.request; + +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +public class ListServicesQueryParametersImpl implements ListServicesQueryParameters { + + private final OptionalValue filter; + private final OptionalValue isDefault; + private final OptionalValue pageSize; + private final OptionalValue page; + + private ListServicesQueryParametersImpl( + OptionalValue filter, + OptionalValue isDefault, + OptionalValue pageSize, + OptionalValue page) { + this.filter = filter; + this.isDefault = isDefault; + this.pageSize = pageSize; + this.page = page; + } + + public OptionalValue getFilter() { + return filter; + } + + public OptionalValue getIsDefault() { + return isDefault; + } + + public OptionalValue getPageSize() { + return pageSize; + } + + public OptionalValue getPage() { + return page; + } + + /** Return true if this ListServicesQueryParameters object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListServicesQueryParametersImpl parameters = (ListServicesQueryParametersImpl) o; + return Objects.equals(this.filter, parameters.filter) + && Objects.equals(this.isDefault, parameters.isDefault) + && Objects.equals(this.pageSize, parameters.pageSize) + && Objects.equals(this.page, parameters.page); + } + + @Override + public int hashCode() { + return Objects.hash(filter, isDefault, pageSize, page); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ListServicesQueryParametersImpl {\n"); + sb.append(" filter: ").append(toIndentedString(filter)).append("\n"); + sb.append(" isDefault: ").append(toIndentedString(isDefault)).append("\n"); + sb.append(" pageSize: ").append(toIndentedString(pageSize)).append("\n"); + sb.append(" page: ").append(toIndentedString(page)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + static class Builder implements ListServicesQueryParameters.Builder { + OptionalValue filter = OptionalValue.empty(); + OptionalValue isDefault = OptionalValue.empty(); + OptionalValue pageSize = OptionalValue.empty(); + OptionalValue page = OptionalValue.empty(); + + protected Builder() {} + + protected Builder(ListServicesQueryParameters _parameters) { + if (null == _parameters) { + return; + } + ListServicesQueryParametersImpl parameters = (ListServicesQueryParametersImpl) _parameters; + this.filter = parameters.getFilter(); + this.isDefault = parameters.getIsDefault(); + this.pageSize = parameters.getPageSize(); + this.page = parameters.getPage(); + } + + public Builder setFilter(String filter) { + this.filter = OptionalValue.of(filter); + return this; + } + + public Builder setIsDefault(Boolean isDefault) { + this.isDefault = OptionalValue.of(isDefault); + return this; + } + + public Builder setPageSize(Integer pageSize) { + this.pageSize = OptionalValue.of(pageSize); + return this; + } + + public Builder setPage(Integer page) { + this.page = OptionalValue.of(page); + return this; + } + + public ListServicesQueryParameters build() { + return new ListServicesQueryParametersImpl(filter, isDefault, pageSize, page); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponse.java new file mode 100644 index 000000000..99583a394 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponse.java @@ -0,0 +1,164 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.time.Instant; + +/** Abbreviated representation of a voice service, returned in list responses. */ +@JsonDeserialize(builder = ServiceShortResponseImpl.Builder.class) +public interface ServiceShortResponse { + + /** + * The ID of the service used. + * + *

Field is required + * + * @return serviceId + */ + String getServiceId(); + + /** + * The ID of the project associated with the call. + * + *

Field is required + * + * @return projectId + */ + String getProjectId(); + + /** + * Timestamp (RFC 3339) indicating when the service was created. + * + *

Field is required + * + * @return createTime + * @readOnly This field is returned by the server and cannot be modified + */ + Instant getCreateTime(); + + /** + * Timestamp (RFC 3339) indicating when the service was last updated. + * + *

Omitted if no updates were performed on this service. + * + * @return updateTime + * @readOnly This field is returned by the server and cannot be modified + */ + Instant getUpdateTime(); + + /** + * Name of the service + * + *

Field is required + * + * @return name + * @readOnly This field is returned by the server and cannot be modified + */ + String getName(); + + /** + * Description of the service + * + * @return description + * @readOnly This field is returned by the server and cannot be modified + */ + String getDescription(); + + /** + * Indicates whether this service is set as the default for the project. The default service is + * used when no specific service is specified in API requests. + * + *

Field is required + * + * @return isDefault + * @readOnly This field is returned by the server and cannot be modified + */ + Boolean getIsDefault(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new ServiceShortResponseImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param serviceId see getter + * @return Current builder + * @see #getServiceId + */ + Builder setServiceId(String serviceId); + + /** + * see getter + * + * @param projectId see getter + * @return Current builder + * @see #getProjectId + */ + Builder setProjectId(String projectId); + + /** + * see getter + * + * @param createTime see getter + * @return Current builder + * @see #getCreateTime + * @readOnly This field is returned by the server and cannot be modified + */ + Builder setCreateTime(Instant createTime); + + /** + * see getter + * + * @param updateTime see getter + * @return Current builder + * @see #getUpdateTime + * @readOnly This field is returned by the server and cannot be modified + */ + Builder setUpdateTime(Instant updateTime); + + /** + * see getter + * + * @param name see getter + * @return Current builder + * @see #getName + * @readOnly This field is returned by the server and cannot be modified + */ + Builder setName(String name); + + /** + * see getter + * + * @param description see getter + * @return Current builder + * @see #getDescription + * @readOnly This field is returned by the server and cannot be modified + */ + Builder setDescription(String description); + + /** + * see getter + * + * @param isDefault see getter + * @return Current builder + * @see #getIsDefault + * @readOnly This field is returned by the server and cannot be modified + */ + Builder setIsDefault(Boolean isDefault); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + ServiceShortResponse build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponseImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponseImpl.java new file mode 100644 index 000000000..f06da0e9c --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponseImpl.java @@ -0,0 +1,252 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.time.Instant; +import java.util.Objects; + +@JsonPropertyOrder({ + ServiceShortResponseImpl.JSON_PROPERTY_SERVICE_ID, + ServiceShortResponseImpl.JSON_PROPERTY_PROJECT_ID, + ServiceShortResponseImpl.JSON_PROPERTY_CREATE_TIME, + ServiceShortResponseImpl.JSON_PROPERTY_UPDATE_TIME, + ServiceShortResponseImpl.JSON_PROPERTY_NAME, + ServiceShortResponseImpl.JSON_PROPERTY_DESCRIPTION, + ServiceShortResponseImpl.JSON_PROPERTY_IS_DEFAULT +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class ServiceShortResponseImpl implements ServiceShortResponse { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_SERVICE_ID = "serviceId"; + + private OptionalValue serviceId; + + public static final String JSON_PROPERTY_PROJECT_ID = "projectId"; + + private OptionalValue projectId; + + public static final String JSON_PROPERTY_CREATE_TIME = "createTime"; + + private OptionalValue createTime; + + public static final String JSON_PROPERTY_UPDATE_TIME = "updateTime"; + + private OptionalValue updateTime; + + public static final String JSON_PROPERTY_NAME = "name"; + + private OptionalValue name; + + public static final String JSON_PROPERTY_DESCRIPTION = "description"; + + private OptionalValue description; + + public static final String JSON_PROPERTY_IS_DEFAULT = "isDefault"; + + private OptionalValue isDefault; + + public ServiceShortResponseImpl() {} + + protected ServiceShortResponseImpl( + OptionalValue serviceId, + OptionalValue projectId, + OptionalValue createTime, + OptionalValue updateTime, + OptionalValue name, + OptionalValue description, + OptionalValue isDefault) { + this.serviceId = serviceId; + this.projectId = projectId; + this.createTime = createTime; + this.updateTime = updateTime; + this.name = name; + this.description = description; + this.isDefault = isDefault; + } + + @JsonIgnore + public String getServiceId() { + return serviceId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SERVICE_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue serviceId() { + return serviceId; + } + + @JsonIgnore + public String getProjectId() { + return projectId.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PROJECT_ID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue projectId() { + return projectId; + } + + @JsonIgnore + public Instant getCreateTime() { + return createTime.orElse(null); + } + + @JsonIgnore + public OptionalValue createTime() { + return createTime; + } + + @JsonIgnore + public Instant getUpdateTime() { + return updateTime.orElse(null); + } + + @JsonIgnore + public OptionalValue updateTime() { + return updateTime; + } + + @JsonIgnore + public String getName() { + return name.orElse(null); + } + + @JsonIgnore + public OptionalValue name() { + return name; + } + + @JsonIgnore + public String getDescription() { + return description.orElse(null); + } + + @JsonIgnore + public OptionalValue description() { + return description; + } + + @JsonIgnore + public Boolean getIsDefault() { + return isDefault.orElse(null); + } + + @JsonIgnore + public OptionalValue isDefault() { + return isDefault; + } + + /** Return true if this ServiceShortResponse object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServiceShortResponseImpl serviceShortResponse = (ServiceShortResponseImpl) o; + return Objects.equals(this.serviceId, serviceShortResponse.serviceId) + && Objects.equals(this.projectId, serviceShortResponse.projectId) + && Objects.equals(this.createTime, serviceShortResponse.createTime) + && Objects.equals(this.updateTime, serviceShortResponse.updateTime) + && Objects.equals(this.name, serviceShortResponse.name) + && Objects.equals(this.description, serviceShortResponse.description) + && Objects.equals(this.isDefault, serviceShortResponse.isDefault); + } + + @Override + public int hashCode() { + return Objects.hash(serviceId, projectId, createTime, updateTime, name, description, isDefault); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ServiceShortResponseImpl {\n"); + sb.append(" serviceId: ").append(toIndentedString(serviceId)).append("\n"); + sb.append(" projectId: ").append(toIndentedString(projectId)).append("\n"); + sb.append(" createTime: ").append(toIndentedString(createTime)).append("\n"); + sb.append(" updateTime: ").append(toIndentedString(updateTime)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" isDefault: ").append(toIndentedString(isDefault)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements ServiceShortResponse.Builder { + OptionalValue serviceId = OptionalValue.empty(); + OptionalValue projectId = OptionalValue.empty(); + OptionalValue createTime = OptionalValue.empty(); + OptionalValue updateTime = OptionalValue.empty(); + OptionalValue name = OptionalValue.empty(); + OptionalValue description = OptionalValue.empty(); + OptionalValue isDefault = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_SERVICE_ID, required = true) + public Builder setServiceId(String serviceId) { + this.serviceId = OptionalValue.of(serviceId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_PROJECT_ID, required = true) + public Builder setProjectId(String projectId) { + this.projectId = OptionalValue.of(projectId); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_CREATE_TIME, required = true) + public Builder setCreateTime(Instant createTime) { + this.createTime = OptionalValue.of(createTime); + return this; + } + + @JsonProperty(JSON_PROPERTY_UPDATE_TIME) + public Builder setUpdateTime(Instant updateTime) { + this.updateTime = OptionalValue.of(updateTime); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_NAME, required = true) + public Builder setName(String name) { + this.name = OptionalValue.of(name); + return this; + } + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + public Builder setDescription(String description) { + this.description = OptionalValue.of(description); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_IS_DEFAULT, required = true) + public Builder setIsDefault(Boolean isDefault) { + this.isDefault = OptionalValue.of(isDefault); + return this; + } + + public ServiceShortResponse build() { + return new ServiceShortResponseImpl( + serviceId, projectId, createTime, updateTime, name, description, isDefault); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServicesListResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServicesListResponse.java new file mode 100644 index 000000000..93028b0eb --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServicesListResponse.java @@ -0,0 +1,48 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response; + +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.models.pagination.ListResponse; +import com.sinch.sdk.core.models.pagination.Page; +import java.util.Collection; +import java.util.Collections; +import java.util.NoSuchElementException; +import java.util.function.Supplier; + +/** Auto paginated response for list of ServiceShortResponse */ +public class ServicesListResponse extends ListResponse { + + private final Page page; + private final Supplier supplier; + + public ServicesListResponse( + Supplier supplier, Page page) { + this.supplier = supplier; + this.page = page; + } + + @Override + public boolean hasNextPage() { + if (null == page.getNextPageToken() || null == getContent() || getContent().isEmpty()) { + return false; + } + return true; + } + + @Override + public ServicesListResponse nextPage() { + if (!hasNextPage()) { + throw new NoSuchElementException("Reached the last page of the API response"); + } + return supplier.get(); + } + + @Override + public Collection getContent() { + return page == null ? Collections.emptyList() : page.getEntities(); + } + + @Override + public String toString() { + return "ServicesListResponse {" + "page=" + page + '}'; + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternal.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternal.java new file mode 100644 index 000000000..93d8aee4a --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternal.java @@ -0,0 +1,86 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response.internal; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import com.sinch.sdk.domains.voice.models.v2.PaginationMeta; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceShortResponse; +import java.util.List; + +/** List of available voice services in the project. */ +@JsonDeserialize(builder = ServicesListResponseInternalImpl.Builder.class) +public interface ServicesListResponseInternal { + + /** + * Get services + * + *

Field is required + * + * @return services + */ + List getServices(); + + /** + * Get links + * + *

Field is required + * + * @return links + */ + PaginationLinks getLinks(); + + /** + * Get meta + * + *

Field is required + * + * @return meta + */ + PaginationMeta getMeta(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new ServicesListResponseInternalImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param services see getter + * @return Current builder + * @see #getServices + */ + Builder setServices(List services); + + /** + * see getter + * + * @param links see getter + * @return Current builder + * @see #getLinks + */ + Builder setLinks(PaginationLinks links); + + /** + * see getter + * + * @param meta see getter + * @return Current builder + * @see #getMeta + */ + Builder setMeta(PaginationMeta meta); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + ServicesListResponseInternal build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternalImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternalImpl.java new file mode 100644 index 000000000..46bb3a91b --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternalImpl.java @@ -0,0 +1,152 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response.internal; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import com.sinch.sdk.domains.voice.models.v2.PaginationMeta; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceShortResponse; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + ServicesListResponseInternalImpl.JSON_PROPERTY_SERVICES, + ServicesListResponseInternalImpl.JSON_PROPERTY_LINKS, + ServicesListResponseInternalImpl.JSON_PROPERTY_META +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class ServicesListResponseInternalImpl implements ServicesListResponseInternal { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_SERVICES = "services"; + + private OptionalValue> services; + + public static final String JSON_PROPERTY_LINKS = "links"; + + private OptionalValue links; + + public static final String JSON_PROPERTY_META = "meta"; + + private OptionalValue meta; + + public ServicesListResponseInternalImpl() {} + + protected ServicesListResponseInternalImpl( + OptionalValue> services, + OptionalValue links, + OptionalValue meta) { + this.services = services; + this.links = links; + this.meta = meta; + } + + @JsonIgnore + public List getServices() { + return services.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SERVICES) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue> services() { + return services; + } + + @JsonIgnore + public PaginationLinks getLinks() { + return links.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_LINKS) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue links() { + return links; + } + + @JsonIgnore + public PaginationMeta getMeta() { + return meta.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_META) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue meta() { + return meta; + } + + /** Return true if this ServicesListResponseInternal object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServicesListResponseInternalImpl servicesListResponseInternal = + (ServicesListResponseInternalImpl) o; + return Objects.equals(this.services, servicesListResponseInternal.services) + && Objects.equals(this.links, servicesListResponseInternal.links) + && Objects.equals(this.meta, servicesListResponseInternal.meta); + } + + @Override + public int hashCode() { + return Objects.hash(services, links, meta); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ServicesListResponseInternalImpl {\n"); + sb.append(" services: ").append(toIndentedString(services)).append("\n"); + sb.append(" links: ").append(toIndentedString(links)).append("\n"); + sb.append(" meta: ").append(toIndentedString(meta)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements ServicesListResponseInternal.Builder { + OptionalValue> services = OptionalValue.empty(); + OptionalValue links = OptionalValue.empty(); + OptionalValue meta = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_SERVICES, required = true) + public Builder setServices(List services) { + this.services = OptionalValue.of(services); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_LINKS, required = true) + public Builder setLinks(PaginationLinks links) { + this.links = OptionalValue.of(links); + return this; + } + + @JsonProperty(value = JSON_PROPERTY_META, required = true) + public Builder setMeta(PaginationMeta meta) { + this.meta = OptionalValue.of(meta); + return this; + } + + public ServicesListResponseInternal build() { + return new ServicesListResponseInternalImpl(services, links, meta); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponseDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponseDtoTest.java new file mode 100644 index 000000000..6b3da027f --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceShortResponseDtoTest.java @@ -0,0 +1,55 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.time.Instant; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class ServiceShortResponseDtoTest extends BaseTest { + + public static final ServiceShortResponse expectedServiceShortResponse = + ServiceShortResponse.builder() + .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setCreateTime(Instant.parse("2025-01-01T00:00:00Z")) + .setUpdateTime(Instant.parse("2025-02-15T10:30:00Z")) + .setName("Primary Service") + .setDescription("Main service with webhook call behavior") + .setIsDefault(true) + .build(); + + public static final ServiceShortResponse expectedServiceShortResponseMinimal = + ServiceShortResponse.builder() + .setServiceId("7f235289-d3ae-57b6-a54d-6d3bf655bbdf") + .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") + .setCreateTime(Instant.parse("2025-02-15T10:30:00Z")) + .setName("Secondary Service") + .setIsDefault(false) + .build(); + + @GivenTextResource("/domains/voice/v2/services/response/ServiceShortResponseDto.json") + String jsonServiceShortResponse; + + @GivenTextResource("/domains/voice/v2/services/response/ServiceShortResponseMinimalDto.json") + String jsonServiceShortResponseMinimal; + + @Test + void deserialize() throws JsonProcessingException { + ServiceShortResponse deserialized = + objectMapper.readValue(jsonServiceShortResponse, ServiceShortResponse.class); + + TestHelpers.recursiveEquals(deserialized, expectedServiceShortResponse); + } + + @Test + void deserializeMinimalDto() throws JsonProcessingException { + ServiceShortResponse deserialized = + objectMapper.readValue(jsonServiceShortResponseMinimal, ServiceShortResponse.class); + + TestHelpers.recursiveEquals(deserialized, expectedServiceShortResponseMinimal); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternalDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternalDtoTest.java new file mode 100644 index 000000000..f49c65a51 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/internal/ServicesListResponseInternalDtoTest.java @@ -0,0 +1,76 @@ +package com.sinch.sdk.domains.voice.models.v2.services.response.internal; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.models.v2.PaginationLinks; +import com.sinch.sdk.domains.voice.models.v2.PaginationMetaDtoTest; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceShortResponseDtoTest; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class ServicesListResponseInternalDtoTest extends BaseTest { + + static final String BASE_URL = + "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services"; + + public static final ServicesListResponseInternal expectedServicesListResponse = + ServicesListResponseInternal.builder() + .setServices( + Arrays.asList( + ServiceShortResponseDtoTest.expectedServiceShortResponse, + ServiceShortResponseDtoTest.expectedServiceShortResponseMinimal)) + .setLinks( + PaginationLinks.builder() + .setFirst(BASE_URL + "?page=1&pageSize=2") + .setLast(BASE_URL + "?page=2&pageSize=2") + .setNext(BASE_URL + "?page=2&pageSize=2") + .setSelf(BASE_URL + "?page=1&pageSize=2") + .build()) + .setMeta(PaginationMetaDtoTest.expectedPaginationMeta) + .build(); + + public static final ServicesListResponseInternal expectedServicesListLastPageResponse = + ServicesListResponseInternal.builder() + .setServices( + Collections.singletonList( + ServiceShortResponseDtoTest.expectedServiceShortResponseMinimal)) + .setLinks( + PaginationLinks.builder() + .setFirst(BASE_URL + "?page=1&pageSize=2") + .setLast(BASE_URL + "?page=2&pageSize=2") + .setPrev(BASE_URL + "?page=1&pageSize=2") + .setSelf(BASE_URL + "?page=2&pageSize=2") + .build()) + .setMeta(PaginationMetaDtoTest.expectedPaginationMeta) + .build(); + + @GivenTextResource( + "/domains/voice/v2/services/response/internal/ServicesListResponseInternalDto.json") + String jsonServicesListResponse; + + @GivenTextResource( + "/domains/voice/v2/services/response/internal/ServicesListResponseInternalLastPageDto.json") + String jsonServicesListLastPageResponse; + + @Test + void deserialize() throws JsonProcessingException { + ServicesListResponseInternal deserialized = + objectMapper.readValue(jsonServicesListResponse, ServicesListResponseInternal.class); + + TestHelpers.recursiveEquals(deserialized, expectedServicesListResponse); + } + + @Test + void deserializeLastPage() throws JsonProcessingException { + ServicesListResponseInternal deserialized = + objectMapper.readValue( + jsonServicesListLastPageResponse, ServicesListResponseInternal.class); + + TestHelpers.recursiveEquals(deserialized, expectedServicesListLastPageResponse); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceShortResponseDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceShortResponseDto.json new file mode 100644 index 000000000..7213476e7 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceShortResponseDto.json @@ -0,0 +1,9 @@ +{ + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "createTime": "2025-01-01T00:00:00Z", + "updateTime": "2025-02-15T10:30:00Z", + "name": "Primary Service", + "description": "Main service with webhook call behavior", + "isDefault": true +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceShortResponseMinimalDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceShortResponseMinimalDto.json new file mode 100644 index 000000000..913100347 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceShortResponseMinimalDto.json @@ -0,0 +1,7 @@ +{ + "serviceId": "7f235289-d3ae-57b6-a54d-6d3bf655bbdf", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "createTime": "2025-02-15T10:30:00Z", + "name": "Secondary Service", + "isDefault": false +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/internal/ServicesListResponseInternalDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/internal/ServicesListResponseInternalDto.json new file mode 100644 index 000000000..88a61f1be --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/internal/ServicesListResponseInternalDto.json @@ -0,0 +1,30 @@ +{ + "services": [ + { + "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "createTime": "2025-01-01T00:00:00Z", + "updateTime": "2025-02-15T10:30:00Z", + "name": "Primary Service", + "description": "Main service with webhook call behavior", + "isDefault": true + }, + { + "serviceId": "7f235289-d3ae-57b6-a54d-6d3bf655bbdf", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "createTime": "2025-02-15T10:30:00Z", + "name": "Secondary Service", + "isDefault": false + } + ], + "links": { + "first": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=1&pageSize=2", + "last": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=2&pageSize=2", + "next": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=2&pageSize=2", + "self": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=1&pageSize=2" + }, + "meta": { + "totalCount": 3, + "pageCount": 2 + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/internal/ServicesListResponseInternalLastPageDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/internal/ServicesListResponseInternalLastPageDto.json new file mode 100644 index 000000000..54b830e02 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/internal/ServicesListResponseInternalLastPageDto.json @@ -0,0 +1,21 @@ +{ + "services": [ + { + "serviceId": "7f235289-d3ae-57b6-a54d-6d3bf655bbdf", + "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", + "createTime": "2025-02-15T10:30:00Z", + "name": "Secondary Service", + "isDefault": false + } + ], + "links": { + "first": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=1&pageSize=2", + "last": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=2&pageSize=2", + "prev": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=1&pageSize=2", + "self": "https://voice.api.sinch.com/v2/projects/5c5bf2b1-35ae-4825-ab89-457e07bb60e6/services?page=2&pageSize=2" + }, + "meta": { + "totalCount": 3, + "pageCount": 2 + } +} From 9c37e922c13079b84a5e7473cbe04577dce18b2a Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 18:04:54 +0200 Subject: [PATCH 34/50] Add Voice v2 services.update endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/ServicesServiceTest.java | 94 +++++++++ .../e2e/domains/voice/v2/ServicesSteps.java | 43 ++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 1 + .../snippets/src/main/java/voice/README.md | 1 + .../main/java/voice/v2/services/Update.java | 51 +++++ .../domains/voice/api/v2/ServicesService.java | 21 ++ .../api/v2/adapters/ServicesServiceImpl.java | 81 +++++++ .../request/UpdateServiceRequest.java | 122 +++++++++++ .../request/UpdateServiceRequestImpl.java | 199 ++++++++++++++++++ .../request/UpdateServiceRequestDtoTest.java | 31 +++ .../request/UpdateServiceRequestDto.json | 10 + 12 files changed, 656 insertions(+) create mode 100644 examples/snippets/src/main/java/voice/v2/services/Update.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/services/request/UpdateServiceRequestDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b0f6789..e5c840744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Services API: `create` operation to create a voice service - **[feature]** Voice V2 Services API: `get` operation to retrieve a voice service by its ID - **[feature]** Voice V2 Services API: `list` operation to list and filter the voice services of a project +- **[feature]** Voice V2 Services API: `update` operation to partially update a voice service - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet @@ -46,6 +47,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/services/Create` snippet - **[doc]** New `voice/v2/services/Get` snippet - **[doc]** New `voice/v2/services/List` snippet +- **[doc]** New `voice/v2/services/Update` snippet - **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java index 262afe34c..9f044c9ef 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java @@ -25,6 +25,8 @@ import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequestDtoTest; import com.sinch.sdk.domains.voice.models.v2.services.request.ListServicesQueryParameters; +import com.sinch.sdk.domains.voice.models.v2.services.request.UpdateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.request.UpdateServiceRequestDtoTest; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponseDtoTest; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceShortResponse; @@ -59,6 +61,9 @@ public class ServicesServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/services/request/CreateServiceRequestDto.json") String jsonCreateServiceRequestDto; + @GivenTextResource("/domains/voice/v2/services/request/UpdateServiceRequestDto.json") + String jsonUpdateServiceRequestDto; + @GivenTextResource("/domains/voice/v2/services/response/ServiceResponseDto.json") String jsonServiceResponseDto; @@ -202,6 +207,95 @@ void getMissingServiceIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + @Test + void update() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/services/" + + URLPathUtils.encodePathSegment("6e124178-c29d-46a5-943c-5c2ae544aade"), + HttpMethod.PATCH, + Collections.emptyList(), + jsonUpdateServiceRequestDto, + Collections.singletonMap( + "Idempotency-Key", + UpdateServiceRequestDtoTest.expectedUpdateServiceRequest.getIdempotencyKey()), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + ServiceResponse response = + service.update( + "6e124178-c29d-46a5-943c-5c2ae544aade", + UpdateServiceRequestDtoTest.expectedUpdateServiceRequest); + + TestHelpers.recursiveEquals(response, ServiceResponseDtoTest.expectedServiceResponse); + } + + @Test + void updateWithoutIdempotencyKeySendsNoHeader() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/services/" + + URLPathUtils.encodePathSegment("6e124178-c29d-46a5-943c-5c2ae544aade"), + HttpMethod.PATCH, + Collections.emptyList(), + "{\"isDefault\":true}", + Collections.emptyMap(), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse(200, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + ServiceResponse response = + service.update( + "6e124178-c29d-46a5-943c-5c2ae544aade", + UpdateServiceRequest.builder().setIsDefault(true).build()); + + TestHelpers.recursiveEquals(response, ServiceResponseDtoTest.expectedServiceResponse); + } + + @Test + void updateMissingServiceIdThrows() { + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> service.update(null, UpdateServiceRequestDtoTest.expectedUpdateServiceRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void updateMissingRequestThrows() { + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, () -> service.update("6e124178-c29d-46a5-943c-5c2ae544aade", null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + @Test void list() throws ApiException { diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java index 512fd49ea..5751a1c7b 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java @@ -2,9 +2,12 @@ import com.sinch.sdk.core.TestHelpers; import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.domains.voice.models.v2.services.EventDestinationCallBehavior; +import com.sinch.sdk.domains.voice.models.v2.services.EventDestinationConfiguration; import com.sinch.sdk.domains.voice.models.v2.services.NoneCallBehavior; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.request.ListServicesQueryParameters; +import com.sinch.sdk.domains.voice.models.v2.services.request.UpdateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; import com.sinch.sdk.domains.voice.models.v2.services.response.ServicesListResponse; import com.sinch.sdk.e2e.Config; @@ -20,6 +23,7 @@ public class ServicesSteps { ServicesService service; ServiceResponse createResponse; ServiceResponse getResponse; + ServiceResponse updateResponse; ServicesListResponse listOnePageResponse; @Given("^the Voice-V2 service \"Services\" is available$") @@ -76,6 +80,45 @@ public void getResult() { TestHelpers.recursiveEquals(getResponse, expected); } + @When("^I send a request to update a Voice-V2 service$") + public void update() { + updateResponse = + service.update( + "3c4d5e6f-7a8b-4901-c234-d5e6f7a8b901", + UpdateServiceRequest.builder() + .setDescription("Service with webhooks") + .setCallBehavior(webhookCallBehavior()) + .setIdempotencyKey("5d8a1f3c-6e2b-4c9d-a7f0-3b1e8c4d2a96") + .build()); + } + + @Then("^the response contains the information about the Voice-V2 service updated$") + public void updateResult() { + ServiceResponse expected = + ServiceResponse.builder() + .setServiceId("3c4d5e6f-7a8b-4901-c234-d5e6f7a8b901") + .setProjectId("c3d4e5f6-a7b8-4901-c234-d5e6f7a8b901") + .setCreateTime(Instant.parse("2026-09-16T10:47:36Z")) + .setUpdateTime(Instant.parse("2026-09-16T10:51:55Z")) + .setName("Example service 2") + .setDescription("Service with webhooks") + .setIsDefault(false) + .setCallBehavior(webhookCallBehavior()) + .build(); + + TestHelpers.recursiveEquals(updateResponse, expected); + } + + private static EventDestinationCallBehavior webhookCallBehavior() { + return EventDestinationCallBehavior.builder() + .setEventDestination( + EventDestinationConfiguration.builder() + .setUrl("https://example.com/webhook") + .setFallbackUrl("https://example.com/fallback") + .build()) + .build(); + } + @When("^I send a request to list Voice-V2 services$") public void listOnePage() { listOnePageResponse = diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 07681c3fd..144603ee4 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -38,6 +38,7 @@ + "|^\\[InteractByCallName\\] interact with an ongoing call by call name$" + "|^\\[Create\\] create a Voice-V2 service$" + "|^\\[Get\\] get Voice-V2 service details$" + + "|^\\[Update\\] update a Voice-V2 service$" + "|^\\[List\\] list (a page of|all the) Voice-V2 services.*$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 56edb3a25..89b6885af 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -40,3 +40,4 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/services/Create](./v2/services/Create.java) - [voice/v2/services/Get](./v2/services/Get.java) - [voice/v2/services/List](./v2/services/List.java) + - [voice/v2/services/Update](./v2/services/Update.java) diff --git a/examples/snippets/src/main/java/voice/v2/services/Update.java b/examples/snippets/src/main/java/voice/v2/services/Update.java new file mode 100644 index 000000000..40520bdb2 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/services/Update.java @@ -0,0 +1,51 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.services; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.domains.voice.models.v2.services.request.UpdateServiceRequest; +import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class Update { + + private static final Logger LOGGER = Logger.getLogger(Update.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the service to update + String serviceId = "SERVICE_ID"; + String name = "Updated service name"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + ServicesService servicesService = client.voice().v2().services(); + + UpdateServiceRequest request = UpdateServiceRequest.builder().setName(name).build(); + + LOGGER.info(String.format("Update voice service with ID '%s'", serviceId)); + + ServiceResponse response = servicesService.update(serviceId, request); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java index 62ac103b3..4684b371a 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java @@ -3,6 +3,7 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.request.ListServicesQueryParameters; +import com.sinch.sdk.domains.voice.models.v2.services.request.UpdateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; import com.sinch.sdk.domains.voice.models.v2.services.response.ServicesListResponse; @@ -69,4 +70,24 @@ public interface ServicesService { * @since 2.3 */ ServicesListResponse list(ListServicesQueryParameters queryParameter) throws ApiException; + + /** + * Update a voice service + * + *

Updates an existing service resource with the provided properties. Only the fields included + * in the request body will be modified; omitted fields remain unchanged. + * + *

To set a service as the default for the project, set isDefault to true + * . Each project can have only one default service: setting a new default automatically + * removes the default status from the previously designated service. isDefault set + * to false is invalid and will not be accepted by the API. + * + * @param serviceId The ID of the service. (required) + * @param updateServiceRequest The properties to update (required) + * @return ServiceResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + ServiceResponse update(String serviceId, UpdateServiceRequest updateServiceRequest) + throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java index e18fa3df9..fb013a03d 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java @@ -18,6 +18,7 @@ import com.sinch.sdk.core.models.pagination.PageNavigator; import com.sinch.sdk.domains.voice.models.v2.services.request.CreateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.request.ListServicesQueryParameters; +import com.sinch.sdk.domains.voice.models.v2.services.request.UpdateServiceRequest; import com.sinch.sdk.domains.voice.models.v2.services.response.ServiceResponse; import com.sinch.sdk.domains.voice.models.v2.services.response.ServicesListResponse; import com.sinch.sdk.domains.voice.models.v2.services.response.internal.ServicesListResponseInternal; @@ -283,4 +284,84 @@ private HttpRequest listRequestBuilder(ListServicesQueryParameters queryParamete localVarContentTypes, localVarAuthNames); } + + @Override + public ServiceResponse update(String serviceId, UpdateServiceRequest updateServiceRequest) + throws ApiException { + + LOGGER.finest( + "[update]" + + " " + + "serviceId: " + + serviceId + + ", " + + "updateServiceRequest: " + + updateServiceRequest); + + HttpRequest httpRequest = updateRequestBuilder(serviceId, updateServiceRequest); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest updateRequestBuilder( + String serviceId, UpdateServiceRequest updateServiceRequest) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling update"); + } + // verify the required parameter 'serviceId' is set + if (serviceId == null) { + throw new ApiException(400, "Missing the required parameter 'serviceId' when calling update"); + } + // verify the required parameter 'updateServiceRequest' is set + if (updateServiceRequest == null) { + throw new ApiException( + 400, "Missing the required parameter 'updateServiceRequest' when calling update"); + } + + String localVarPath = + "/v2/projects/{projectId}/services/{serviceId}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "serviceId" + "\\}", URLPathUtils.encodePathSegment(serviceId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + if (updateServiceRequest.getIdempotencyKey() != null) + localVarHeaderParams.put("Idempotency-Key", updateServiceRequest.getIdempotencyKey()); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList("application/json"); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = mapper.serialize(localVarContentTypes, updateServiceRequest); + + return new HttpRequest( + localVarPath, + HttpMethod.PATCH, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java new file mode 100644 index 000000000..9f342e488 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java @@ -0,0 +1,122 @@ +package com.sinch.sdk.domains.voice.models.v2.services.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.services.CallBehavior; + +/** + * Request body for updating an existing voice service. Only the fields provided will be modified; + * omitted fields remain unchanged. + */ +@JsonDeserialize(builder = UpdateServiceRequestImpl.Builder.class) +public interface UpdateServiceRequest { + + /** + * Voice service name. No leading or trailing whitespaces allowed. + * + * @return name + */ + String getName(); + + /** + * Description of the service + * + * @return description + */ + String getDescription(); + + /** + * Indicates whether this service is set as the default for the project. The default service is + * used when no specific service is specified in API requests. + * + *

Each project can have only one default service: setting a new default removes the default + * status from the previously designated service. This property cannot be set to false + * : in such case, a BadRequest will be returned by the API. + * + * @return isDefault + */ + Boolean getIsDefault(); + + /** + * Defines how calls are handled for this service + * + * @return callBehavior + */ + CallBehavior getCallBehavior(); + + /** + * Client-generated idempotency key to safely retry requests. The server uses this key to + * recognize retries of the same request. If a request with the same key is received within 10 + * minutes, the server returns the cached response from the original request. Using a random UUID + * (v4) is strongly recommended. + * + *

Sent as the Idempotency-Key header. + * + * @return idempotencyKey + */ + String getIdempotencyKey(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new UpdateServiceRequestImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param name see getter + * @return Current builder + * @see #getName + */ + Builder setName(String name); + + /** + * see getter + * + * @param description see getter + * @return Current builder + * @see #getDescription + */ + Builder setDescription(String description); + + /** + * see getter + * + * @param isDefault see getter + * @return Current builder + * @see #getIsDefault + */ + Builder setIsDefault(Boolean isDefault); + + /** + * see getter + * + * @param callBehavior see getter + * @return Current builder + * @see #getCallBehavior + */ + Builder setCallBehavior(CallBehavior callBehavior); + + /** + * see getter + * + * @param idempotencyKey see getter + * @return Current builder + * @see #getIdempotencyKey + */ + Builder setIdempotencyKey(String idempotencyKey); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + UpdateServiceRequest build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestImpl.java new file mode 100644 index 000000000..098289f41 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestImpl.java @@ -0,0 +1,199 @@ +package com.sinch.sdk.domains.voice.models.v2.services.request; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.services.CallBehavior; +import java.util.Objects; + +@JsonPropertyOrder({ + UpdateServiceRequestImpl.JSON_PROPERTY_NAME, + UpdateServiceRequestImpl.JSON_PROPERTY_DESCRIPTION, + UpdateServiceRequestImpl.JSON_PROPERTY_IS_DEFAULT, + UpdateServiceRequestImpl.JSON_PROPERTY_CALL_BEHAVIOR +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class UpdateServiceRequestImpl implements UpdateServiceRequest { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_NAME = "name"; + + private OptionalValue name; + + public static final String JSON_PROPERTY_DESCRIPTION = "description"; + + private OptionalValue description; + + public static final String JSON_PROPERTY_IS_DEFAULT = "isDefault"; + + private OptionalValue isDefault; + + public static final String JSON_PROPERTY_CALL_BEHAVIOR = "callBehavior"; + + private OptionalValue callBehavior; + + private OptionalValue idempotencyKey; + + public UpdateServiceRequestImpl() {} + + protected UpdateServiceRequestImpl( + OptionalValue name, + OptionalValue description, + OptionalValue isDefault, + OptionalValue callBehavior, + OptionalValue idempotencyKey) { + this.name = name; + this.description = description; + this.isDefault = isDefault; + this.callBehavior = callBehavior; + this.idempotencyKey = idempotencyKey; + } + + @JsonIgnore + public String getName() { + return name.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_NAME) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue name() { + return name; + } + + @JsonIgnore + public String getDescription() { + return description.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue description() { + return description; + } + + @JsonIgnore + public Boolean getIsDefault() { + return isDefault.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_IS_DEFAULT) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue isDefault() { + return isDefault; + } + + @JsonIgnore + public CallBehavior getCallBehavior() { + return callBehavior.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_CALL_BEHAVIOR) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue callBehavior() { + return callBehavior; + } + + @JsonIgnore + public String getIdempotencyKey() { + return idempotencyKey.orElse(null); + } + + @JsonIgnore + public OptionalValue idempotencyKey() { + return idempotencyKey; + } + + /** Return true if this UpdateServiceRequest object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UpdateServiceRequestImpl updateServiceRequest = (UpdateServiceRequestImpl) o; + return Objects.equals(this.name, updateServiceRequest.name) + && Objects.equals(this.description, updateServiceRequest.description) + && Objects.equals(this.isDefault, updateServiceRequest.isDefault) + && Objects.equals(this.callBehavior, updateServiceRequest.callBehavior) + && Objects.equals(this.idempotencyKey, updateServiceRequest.idempotencyKey); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, isDefault, callBehavior, idempotencyKey); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class UpdateServiceRequestImpl {\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" isDefault: ").append(toIndentedString(isDefault)).append("\n"); + sb.append(" callBehavior: ").append(toIndentedString(callBehavior)).append("\n"); + sb.append(" idempotencyKey: ").append(toIndentedString(idempotencyKey)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements UpdateServiceRequest.Builder { + OptionalValue name = OptionalValue.empty(); + OptionalValue description = OptionalValue.empty(); + OptionalValue isDefault = OptionalValue.empty(); + OptionalValue callBehavior = OptionalValue.empty(); + OptionalValue idempotencyKey = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_NAME) + public Builder setName(String name) { + this.name = OptionalValue.of(name); + return this; + } + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + public Builder setDescription(String description) { + this.description = OptionalValue.of(description); + return this; + } + + @JsonProperty(JSON_PROPERTY_IS_DEFAULT) + public Builder setIsDefault(Boolean isDefault) { + this.isDefault = OptionalValue.of(isDefault); + return this; + } + + @JsonProperty(JSON_PROPERTY_CALL_BEHAVIOR) + public Builder setCallBehavior(CallBehavior callBehavior) { + this.callBehavior = OptionalValue.of(callBehavior); + return this; + } + + @JsonIgnore + public Builder setIdempotencyKey(String idempotencyKey) { + this.idempotencyKey = OptionalValue.of(idempotencyKey); + return this; + } + + public UpdateServiceRequest build() { + return new UpdateServiceRequestImpl( + name, description, isDefault, callBehavior, idempotencyKey); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestDtoTest.java new file mode 100644 index 000000000..7ec544505 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestDtoTest.java @@ -0,0 +1,31 @@ +package com.sinch.sdk.domains.voice.models.v2.services.request; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.services.EventDestinationCallBehaviorDtoTest; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class UpdateServiceRequestDtoTest extends BaseTest { + + public static final UpdateServiceRequest expectedUpdateServiceRequest = + UpdateServiceRequest.builder() + .setDescription("Service with webhooks") + .setCallBehavior(EventDestinationCallBehaviorDtoTest.expectedEventDestinationCallBehavior) + .setIdempotencyKey("7b2e9f4a-3c8d-4a1e-b6f5-0d9c2a7e4b13") + .build(); + + @GivenTextResource("/domains/voice/v2/services/request/UpdateServiceRequestDto.json") + String jsonUpdateServiceRequest; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedUpdateServiceRequest); + + JSONAssert.assertEquals(jsonUpdateServiceRequest, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/request/UpdateServiceRequestDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/request/UpdateServiceRequestDto.json new file mode 100644 index 000000000..a3991d977 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/request/UpdateServiceRequestDto.json @@ -0,0 +1,10 @@ +{ + "description": "Service with webhooks", + "callBehavior": { + "type": "WEBHOOK", + "webhook": { + "url": "https://example.com/webhook", + "fallbackUrl": "https://example.com/fallback" + } + } +} From a25147a285e7f79b9d9ff58af3672f7e93a55bd6 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 8 Oct 2026 18:48:04 +0200 Subject: [PATCH 35/50] Add Voice v2 services.delete endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/ServicesServiceTest.java | 50 +++++++++++++++ .../e2e/domains/voice/v2/ServicesSteps.java | 12 ++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 1 + .../snippets/src/main/java/voice/README.md | 1 + .../main/java/voice/v2/services/Delete.java | 46 +++++++++++++ .../domains/voice/api/v2/ServicesService.java | 15 +++++ .../api/v2/adapters/ServicesServiceImpl.java | 64 +++++++++++++++++++ 8 files changed, 191 insertions(+) create mode 100644 examples/snippets/src/main/java/voice/v2/services/Delete.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c840744..0e4922268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Services API: `get` operation to retrieve a voice service by its ID - **[feature]** Voice V2 Services API: `list` operation to list and filter the voice services of a project - **[feature]** Voice V2 Services API: `update` operation to partially update a voice service +- **[feature]** Voice V2 Services API: `delete` operation to delete a voice service by its ID - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet @@ -48,6 +49,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/services/Get` snippet - **[doc]** New `voice/v2/services/List` snippet - **[doc]** New `voice/v2/services/Update` snippet +- **[doc]** New `voice/v2/services/Delete` snippet - **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java index 9f044c9ef..7ebab236c 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java @@ -169,6 +169,56 @@ void createMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + @Test + void delete() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + + URLPathUtils.encodePathSegment(PROJECT_ID) + + "/services/" + + URLPathUtils.encodePathSegment("6e124178-c29d-46a5-943c-5c2ae544aade"), + HttpMethod.DELETE, + Collections.emptyList(), + (String) null, + Collections.emptyMap(), + ACCEPTS, + Collections.emptyList(), + AUTH_NAMES); + HttpResponse httpResponse = new HttpResponse(204, null, Collections.emptyMap(), null); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + service.delete("6e124178-c29d-46a5-943c-5c2ae544aade"); + } + + @Test + void deleteMissingServiceIdThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.delete(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void deleteMissingProjectIdThrows() { + + ServicesService serviceWithoutProjectId = + new ServicesServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> serviceWithoutProjectId.delete("6e124178-c29d-46a5-943c-5c2ae544aade")); + + Assertions.assertEquals(400, thrown.getCode()); + } + @Test void get() throws ApiException { diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java index 5751a1c7b..65749495a 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java @@ -24,6 +24,7 @@ public class ServicesSteps { ServiceResponse createResponse; ServiceResponse getResponse; ServiceResponse updateResponse; + Boolean deletePassed; ServicesListResponse listOnePageResponse; @Given("^the Voice-V2 service \"Services\" is available$") @@ -109,6 +110,17 @@ public void updateResult() { TestHelpers.recursiveEquals(updateResponse, expected); } + @When("^I send a request to delete a Voice-V2 service$") + public void delete() { + service.delete("3c4d5e6f-7a8b-4901-c234-d5e6f7a8b901"); + deletePassed = true; + } + + @Then("^the response confirms the Voice-V2 service was deleted$") + public void deleteResult() { + Assertions.assertTrue(deletePassed); + } + private static EventDestinationCallBehavior webhookCallBehavior() { return EventDestinationCallBehavior.builder() .setEventDestination( diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 144603ee4..812d5eb45 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -39,6 +39,7 @@ + "|^\\[Create\\] create a Voice-V2 service$" + "|^\\[Get\\] get Voice-V2 service details$" + "|^\\[Update\\] update a Voice-V2 service$" + + "|^\\[Delete\\] delete a Voice-V2 service$" + "|^\\[List\\] list (a page of|all the) Voice-V2 services.*$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index 89b6885af..cf28e80be 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -38,6 +38,7 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/sessions/Get](./v2/sessions/Get.java) - Services - [voice/v2/services/Create](./v2/services/Create.java) + - [voice/v2/services/Delete](./v2/services/Delete.java) - [voice/v2/services/Get](./v2/services/Get.java) - [voice/v2/services/List](./v2/services/List.java) - [voice/v2/services/Update](./v2/services/Update.java) diff --git a/examples/snippets/src/main/java/voice/v2/services/Delete.java b/examples/snippets/src/main/java/voice/v2/services/Delete.java new file mode 100644 index 000000000..cbe174ff5 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/services/Delete.java @@ -0,0 +1,46 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.services; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.ServicesService; +import com.sinch.sdk.models.Configuration; +import java.util.logging.Logger; +import utils.Settings; + +public class Delete { + + private static final Logger LOGGER = Logger.getLogger(Delete.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + // The ID of the service to delete + String serviceId = "SERVICE_ID"; + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + ServicesService servicesService = client.voice().v2().services(); + + LOGGER.info(String.format("Deleting voice service with ID '%s'", serviceId)); + + servicesService.delete(serviceId); + + LOGGER.info("Done"); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java index 4684b371a..58e68a37b 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/ServicesService.java @@ -22,6 +22,21 @@ public interface ServicesService { */ ServiceResponse create(CreateServiceRequest createServiceRequest) throws ApiException; + /** + * Delete a voice service by ID + * + *

Deletes a service permanently. + * + *

Important: The default service cannot be deleted. To delete the current + * default service, a different service must first be designated as the default using the PATCH + * endpoint. + * + * @param serviceId The ID of the service. (required) + * @throws ApiException if fails to make API call + * @since 2.3 + */ + void delete(String serviceId) throws ApiException; + /** * Retrieve a voice service by ID * diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java index fb013a03d..a102a5647 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java @@ -119,6 +119,70 @@ private HttpRequest createRequestBuilder(CreateServiceRequest createServiceReque localVarAuthNames); } + @Override + public void delete(String serviceId) throws ApiException { + + LOGGER.finest("[delete]" + " " + "serviceId: " + serviceId); + + HttpRequest httpRequest = deleteRequestBuilder(serviceId); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return; + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest deleteRequestBuilder(String serviceId) throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling delete"); + } + // verify the required parameter 'serviceId' is set + if (serviceId == null) { + throw new ApiException(400, "Missing the required parameter 'serviceId' when calling delete"); + } + + String localVarPath = + "/v2/projects/{projectId}/services/{serviceId}" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())) + .replaceAll( + "\\{" + "serviceId" + "\\}", URLPathUtils.encodePathSegment(serviceId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList(); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = null; + + return new HttpRequest( + localVarPath, + HttpMethod.DELETE, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } + @Override public ServiceResponse get(String serviceId) throws ApiException { From 150ec968da302c6dcc9aed0e37229e89b6373d2d Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Fri, 9 Oct 2026 12:06:02 +0200 Subject: [PATCH 36/50] Use setAsDefault on service update and Java SDK names in services snippets --- .../domains/voice/api/v2/adapters/ServicesServiceTest.java | 2 +- .../snippets/src/main/java/voice/v2/services/Create.java | 2 +- .../snippets/src/main/java/voice/v2/services/Update.java | 2 +- .../models/v2/services/request/UpdateServiceRequest.java | 7 ++++--- .../v2/services/request/UpdateServiceRequestImpl.java | 7 ++++++- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java index 7ebab236c..48f074907 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java @@ -320,7 +320,7 @@ void updateWithoutIdempotencyKeySendsNoHeader() throws ApiException { ServiceResponse response = service.update( "6e124178-c29d-46a5-943c-5c2ae544aade", - UpdateServiceRequest.builder().setIsDefault(true).build()); + UpdateServiceRequest.builder().setAsDefault().build()); TestHelpers.recursiveEquals(response, ServiceResponseDtoTest.expectedServiceResponse); } diff --git a/examples/snippets/src/main/java/voice/v2/services/Create.java b/examples/snippets/src/main/java/voice/v2/services/Create.java index 3e9de61a9..677067300 100644 --- a/examples/snippets/src/main/java/voice/v2/services/Create.java +++ b/examples/snippets/src/main/java/voice/v2/services/Create.java @@ -25,7 +25,7 @@ public static void main(String[] args) { String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); - String name = "Example service"; + String name = "Created from Java SDK snippet"; Configuration configuration = Configuration.builder() diff --git a/examples/snippets/src/main/java/voice/v2/services/Update.java b/examples/snippets/src/main/java/voice/v2/services/Update.java index 40520bdb2..372976e62 100644 --- a/examples/snippets/src/main/java/voice/v2/services/Update.java +++ b/examples/snippets/src/main/java/voice/v2/services/Update.java @@ -27,7 +27,7 @@ public static void main(String[] args) { // The ID of the service to update String serviceId = "SERVICE_ID"; - String name = "Updated service name"; + String name = "Updated from Java SDK snippet"; Configuration configuration = Configuration.builder() diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java index 9f342e488..141de1ffe 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java @@ -86,13 +86,14 @@ interface Builder { Builder setDescription(String description); /** - * see getter + * Set this service as the default for the project + * + *

The API does not accept false on update, so there is no value to pass * - * @param isDefault see getter * @return Current builder * @see #getIsDefault */ - Builder setIsDefault(Boolean isDefault); + Builder setAsDefault(); /** * see getter diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestImpl.java index 098289f41..ef214fe66 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequestImpl.java @@ -173,8 +173,13 @@ public Builder setDescription(String description) { return this; } + public Builder setAsDefault() { + return setIsDefault(true); + } + + // used by Jackson only: the public builder exposes setAsDefault() @JsonProperty(JSON_PROPERTY_IS_DEFAULT) - public Builder setIsDefault(Boolean isDefault) { + Builder setIsDefault(Boolean isDefault) { this.isDefault = OptionalValue.of(isDefault); return this; } From ce145e566c17e5d8e299a62c21b270772699b8b4 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Fri, 9 Oct 2026 09:27:19 +0200 Subject: [PATCH 37/50] Add Voice v2 svaml.describe endpoint --- CHANGELOG.md | 2 + .../domains/voice/api/v2/VoiceService.java | 8 ++ .../voice/api/v2/adapters/VoiceService.java | 20 +++ .../models/v2/svaml/request/package-info.java | 6 + .../v2/svaml/response/package-info.java | 6 + .../api/v2/adapters/SvamlServiceTest.java | 116 ++++++++++++++++++ .../api/v2/adapters/VoiceServiceTest.java | 5 + .../sdk/e2e/domains/voice/v2/SvamlSteps.java | 92 ++++++++++++++ .../sdk/e2e/domains/voice/v2/VoiceIT.java | 2 + .../snippets/src/main/java/voice/README.md | 2 + .../main/java/voice/v2/svaml/Describe.java | 66 ++++++++++ .../domains/voice/api/v2/SvamlService.java | 23 ++++ .../api/v2/adapters/SvamlServiceImpl.java | 113 +++++++++++++++++ .../svaml/request/DescribeSvamlRequest.java | 47 +++++++ .../request/DescribeSvamlRequestImpl.java | 91 ++++++++++++++ .../response/SvamlDescriptionResponse.java | 44 +++++++ .../SvamlDescriptionResponseImpl.java | 90 ++++++++++++++ .../request/DescribeSvamlRequestDtoTest.java | 27 ++++ .../SvamlDescriptionResponseDtoTest.java | 28 +++++ .../request/DescribeSvamlRequestDto.json | 19 +++ .../response/SvamlDescriptionResponseDto.json | 3 + 21 files changed, 810 insertions(+) create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/package-info.java create mode 100644 client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/package-info.java create mode 100644 client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceTest.java create mode 100644 client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SvamlSteps.java create mode 100644 examples/snippets/src/main/java/voice/v2/svaml/Describe.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SvamlService.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequest.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequestImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponseImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequestDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponseDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/request/DescribeSvamlRequestDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/response/SvamlDescriptionResponseDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4922268..91a14dc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Services API: `list` operation to list and filter the voice services of a project - **[feature]** Voice V2 Services API: `update` operation to partially update a voice service - **[feature]** Voice V2 Services API: `delete` operation to delete a voice service by its ID +- **[feature]** Voice V2 Svaml API: `describe` operation to get a human-readable description of the call flow of a SVAML payload - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet @@ -50,6 +51,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/services/List` snippet - **[doc]** New `voice/v2/services/Update` snippet - **[doc]** New `voice/v2/services/Delete` snippet +- **[doc]** New `voice/v2/svaml/Describe` snippet - **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java index f18e67910..4f9c1f46e 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/VoiceService.java @@ -41,6 +41,14 @@ public interface VoiceService { */ ServicesService services(); + /** + * Svaml Service instance + * + * @return service instance for project + * @since 2.3 + */ + SvamlService svaml(); + /** * Sinch Events Service instance * diff --git a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java index c78cef676..3dbaed8e0 100644 --- a/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java +++ b/client/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceService.java @@ -10,6 +10,7 @@ import com.sinch.sdk.domains.voice.api.v2.CallsService; import com.sinch.sdk.domains.voice.api.v2.ServicesService; import com.sinch.sdk.domains.voice.api.v2.SessionsService; +import com.sinch.sdk.domains.voice.api.v2.SvamlService; import com.sinch.sdk.domains.voice.models.v2.VoiceContext; import com.sinch.sdk.models.UnifiedCredentials; import java.util.AbstractMap; @@ -37,6 +38,7 @@ public class VoiceService implements com.sinch.sdk.domains.voice.api.v2.VoiceSer private volatile BatchesService batches; private volatile SessionsService sessions; private volatile ServicesService services; + private volatile SvamlService svaml; private volatile SinchEventsService sinchEvents; public VoiceService( @@ -122,6 +124,24 @@ public ServicesService services() { return this.services; } + public SvamlService svaml() { + if (null == this.svaml) { + synchronized (this) { + if (null == this.svaml) { + instanceLazyInit(); + this.svaml = + new SvamlServiceImpl( + httpClientSupplier.get(), + context.getVoiceServer(), + authManagers, + HttpMapper.getInstance(), + uriUUID); + } + } + } + return this.svaml; + } + public SinchEventsService sinchEvents() { // no lazy init: validating a Sinch Event takes service credentials on each call if (null == this.sinchEvents) { diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/package-info.java new file mode 100644 index 000000000..f768ad546 --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 SVAML requests + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.svaml.request; diff --git a/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/package-info.java b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/package-info.java new file mode 100644 index 000000000..fc57b066a --- /dev/null +++ b/client/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/package-info.java @@ -0,0 +1,6 @@ +/** + * Models related to Voice V2 SVAML responses + * + * @since 2.3 + */ +package com.sinch.sdk.domains.voice.models.v2.svaml.response; diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceTest.java new file mode 100644 index 000000000..9d9ad04d4 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceTest.java @@ -0,0 +1,116 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpContentType; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.HttpRequestTest.HttpRequestMatcher; +import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.URLPathUtils; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.domains.voice.api.v2.SvamlService; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponseDtoTest; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +@TestWithResources +public class SvamlServiceTest extends BaseTest { + + @Mock HttpClient httpClient; + @Mock ServerConfiguration serverConfiguration; + @Mock Map authManagers; + + static final String PROJECT_ID = "test_project_id"; + static final Collection AUTH_NAMES = Arrays.asList("BasicAuth", "SinchOAuth2"); + static final Collection ACCEPTS = + Arrays.asList(HttpContentType.APPLICATION_JSON, "application/problem+json"); + + SvamlService service; + + @GivenTextResource("/domains/voice/v2/svaml/request/DescribeSvamlRequestDto.json") + String jsonDescribeSvamlRequestDto; + + @GivenTextResource("/domains/voice/v2/svaml/response/SvamlDescriptionResponseDto.json") + String jsonSvamlDescriptionResponseDto; + + @BeforeEach + public void initMocks() { + service = + new SvamlServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), PROJECT_ID); + } + + @Test + void describe() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/svaml/describe", + HttpMethod.POST, + Collections.emptyList(), + jsonDescribeSvamlRequestDto, + Collections.emptyMap(), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse( + 200, null, Collections.emptyMap(), jsonSvamlDescriptionResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + SvamlDescriptionResponse response = + service.describe(DescribeSvamlRequestDtoTest.expectedDescribeSvamlRequest); + + TestHelpers.recursiveEquals( + response, SvamlDescriptionResponseDtoTest.expectedSvamlDescriptionResponse); + } + + @Test + void describeMissingRequestThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.describe(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void describeMissingProjectIdThrows() { + + SvamlService serviceWithoutProjectId = + new SvamlServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> + serviceWithoutProjectId.describe( + DescribeSvamlRequestDtoTest.expectedDescribeSvamlRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } +} diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java index ecf1a9b36..679141619 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/VoiceServiceTest.java @@ -31,6 +31,11 @@ void checkCredentialsServices() { CredentialsValidationHelper.checkCredentials(() -> httpClient, VoiceService::services); } + @Test + void checkCredentialsSvaml() { + CredentialsValidationHelper.checkCredentials(() -> httpClient, VoiceService::svaml); + } + @Test void sinchEventsRequireNoCredentials() { Assertions.assertNotNull( diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SvamlSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SvamlSteps.java new file mode 100644 index 000000000..4d42947ea --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SvamlSteps.java @@ -0,0 +1,92 @@ +package com.sinch.sdk.e2e.domains.voice.v2; + +import com.sinch.sdk.core.TestHelpers; +import com.sinch.sdk.domains.voice.api.v2.SvamlService; +import com.sinch.sdk.domains.voice.models.v2.destination.Phone; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.CallEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.DialCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.calls.HangupCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessageEvents; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.PlayMessage; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; +import com.sinch.sdk.e2e.Config; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.Assertions; + +public class SvamlSteps { + + SvamlService service; + SvamlDescriptionResponse describeResponse; + + @Given("^the Voice-V2 service \"Svaml\" is available$") + public void serviceAvailable() { + service = Config.getSinchClient().voice().v2().svaml(); + Assertions.assertNotNull(service, "Voice V2 Svaml service is not available"); + } + + @When("^I send a request to describe a SVAML payload$") + public void describe() { + MessagesCommand notification = + MessagesCommand.builder() + .setMessagesName("notification") + .setMessages( + Arrays.asList( + PlayMessage.builder() + .setUrl("https://samplelib.com/mp3/sample-12s.mp3") + .build(), + SayMessage.builder() + .setText( + "Hello! This is a test notification from Sinch. Your" + + " verification code is 4 8 3 7.") + .setVoiceName("Emma") + .build())) + .setEvents( + MessageEvents.builder() + .setOnFinish(Collections.singletonList(HangupCommand.builder().build())) + .build()) + .build(); + + DialCommand dial = + DialCommand.builder() + .setCallName("audio-notification") + .setFrom(Phone.builder().setNumber("+12015555555").build()) + .setTo(Phone.builder().setNumber("+12017777777").build()) + .setDialTimeoutDurationSeconds(30) + .setMaxCallDurationSeconds(300) + .setEvents( + CallEvents.builder().setOnAnswer(Collections.singletonList(notification)).build()) + .build(); + + describeResponse = + service.describe( + DescribeSvamlRequest.builder() + .setSvaml(SvamlInput.builder().setCommands(Collections.singletonList(dial)).build()) + .build()); + } + + @Then("^the response contains the description of the SVAML payload$") + public void describeResult() { + SvamlDescriptionResponse expected = + SvamlDescriptionResponse.builder() + .setDescription( + "1. A new call with name 'audio-notification' will be initiated to number" + + " +12017777777 with max duration set to 5 minutes.\n" + + " * on answer:\n" + + " 1. An audio file will be played from" + + " https://samplelib.com/mp3/sample-12s.mp3.\n" + + " 1. A TTS message will be played using the voice Emma.\n" + + " * on finish:\n" + + " 1. The call will be disconnected") + .build(); + + TestHelpers.recursiveEquals(describeResponse, expected); + } +} diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index 812d5eb45..d6a5867aa 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -25,6 +25,7 @@ @SelectClasspathResource("features/voice-v2/batches.feature") @SelectClasspathResource("features/voice-v2/sessions.feature") @SelectClasspathResource("features/voice-v2/services.feature") +@SelectClasspathResource("features/voice-v2/payloads.feature") @SelectClasspathResource("features/voice-v2/webhooks-events.feature") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.sinch.sdk.e2e.domains.voice.v2") @ConfigurationParameter( @@ -41,6 +42,7 @@ + "|^\\[Update\\] update a Voice-V2 service$" + "|^\\[Delete\\] delete a Voice-V2 service$" + "|^\\[List\\] list (a page of|all the) Voice-V2 services.*$" + + "|^\\[Describe\\] describe a SVAML payload$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") public class VoiceIT {} diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index cf28e80be..b751941c9 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -42,3 +42,5 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/services/Get](./v2/services/Get.java) - [voice/v2/services/List](./v2/services/List.java) - [voice/v2/services/Update](./v2/services/Update.java) + - Svaml + - [voice/v2/svaml/Describe](./v2/svaml/Describe.java) diff --git a/examples/snippets/src/main/java/voice/v2/svaml/Describe.java b/examples/snippets/src/main/java/voice/v2/svaml/Describe.java new file mode 100644 index 000000000..647173dbe --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/svaml/Describe.java @@ -0,0 +1,66 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.svaml; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.SvamlService; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; +import com.sinch.sdk.models.Configuration; +import java.util.Collections; +import java.util.logging.Logger; +import utils.Settings; + +public class Describe { + + private static final Logger LOGGER = Logger.getLogger(Describe.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + SvamlService svamlService = client.voice().v2().svaml(); + + DescribeSvamlRequest request = + DescribeSvamlRequest.builder() + .setSvaml( + SvamlInput.builder() + .setCommands( + Collections.singletonList( + MessagesCommand.builder() + .setMessages( + Collections.singletonList( + SayMessage.builder() + .setText("Hello, your call is now connected.") + .setVoiceName("Emma") + .build())) + .build())) + .build()) + .build(); + + LOGGER.info("Describe a SVAML payload"); + + SvamlDescriptionResponse response = svamlService.describe(request); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SvamlService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SvamlService.java new file mode 100644 index 000000000..605e15048 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SvamlService.java @@ -0,0 +1,23 @@ +package com.sinch.sdk.domains.voice.api.v2; + +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; + +/** Svaml Service */ +public interface SvamlService { + + /** + * Describe the call flow from the SVAML payload + * + *

This endpoint is useful for understanding the structure and flow of a SVAML payload without + * executing it. It provides a detailed description of the commands, events, and messages defined + * in the SVAML. + * + * @param describeSvamlRequest The SVAML payload to describe (required) + * @return SvamlDescriptionResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + SvamlDescriptionResponse describe(DescribeSvamlRequest describeSvamlRequest) throws ApiException; +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceImpl.java new file mode 100644 index 000000000..0785407d5 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceImpl.java @@ -0,0 +1,113 @@ +package com.sinch.sdk.domains.voice.api.v2.adapters; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.sinch.sdk.core.exceptions.ApiException; +import com.sinch.sdk.core.exceptions.ApiExceptionBuilder; +import com.sinch.sdk.core.http.AuthManager; +import com.sinch.sdk.core.http.HttpClient; +import com.sinch.sdk.core.http.HttpMapper; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.HttpResponse; +import com.sinch.sdk.core.http.HttpStatus; +import com.sinch.sdk.core.http.URLParameter; +import com.sinch.sdk.core.http.URLPathUtils; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +public class SvamlServiceImpl implements com.sinch.sdk.domains.voice.api.v2.SvamlService { + + private static final Logger LOGGER = Logger.getLogger(SvamlServiceImpl.class.getName()); + private final HttpClient httpClient; + private final ServerConfiguration serverConfiguration; + private final Map authManagersByOasSecuritySchemes; + private final HttpMapper mapper; + + private final String projectId; + + public SvamlServiceImpl( + HttpClient httpClient, + ServerConfiguration serverConfiguration, + Map authManagersByOasSecuritySchemes, + HttpMapper mapper, + String projectId) { + this.httpClient = httpClient; + this.serverConfiguration = serverConfiguration; + this.authManagersByOasSecuritySchemes = authManagersByOasSecuritySchemes; + this.mapper = mapper; + this.projectId = projectId; + } + + @Override + public SvamlDescriptionResponse describe(DescribeSvamlRequest describeSvamlRequest) + throws ApiException { + + LOGGER.finest("[describe]" + " " + "describeSvamlRequest: " + describeSvamlRequest); + + HttpRequest httpRequest = describeRequestBuilder(describeSvamlRequest); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest describeRequestBuilder(DescribeSvamlRequest describeSvamlRequest) + throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling describe"); + } + // verify the required parameter 'describeSvamlRequest' is set + if (describeSvamlRequest == null) { + throw new ApiException( + 400, "Missing the required parameter 'describeSvamlRequest' when calling describe"); + } + + String localVarPath = + "/v2/projects/{projectId}/svaml/describe" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList("application/json"); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = mapper.serialize(localVarContentTypes, describeSvamlRequest); + + return new HttpRequest( + localVarPath, + HttpMethod.POST, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequest.java new file mode 100644 index 000000000..e2c1eda4f --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequest.java @@ -0,0 +1,47 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; + +/** Request payload for describing a SVAML document before using it in a call. */ +@JsonDeserialize(builder = DescribeSvamlRequestImpl.Builder.class) +public interface DescribeSvamlRequest { + + /** + * The SVAML payload to describe. + * + *

Field is required + * + * @return svaml + */ + SvamlInput getSvaml(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new DescribeSvamlRequestImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param svaml see getter + * @return Current builder + * @see #getSvaml + */ + Builder setSvaml(SvamlInput svaml); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + DescribeSvamlRequest build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequestImpl.java new file mode 100644 index 000000000..07fd5cdae --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequestImpl.java @@ -0,0 +1,91 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.request; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import java.util.Objects; + +@JsonPropertyOrder({DescribeSvamlRequestImpl.JSON_PROPERTY_SVAML}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class DescribeSvamlRequestImpl implements DescribeSvamlRequest { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_SVAML = "svaml"; + + private OptionalValue svaml; + + public DescribeSvamlRequestImpl() {} + + protected DescribeSvamlRequestImpl(OptionalValue svaml) { + this.svaml = svaml; + } + + @JsonIgnore + public SvamlInput getSvaml() { + return svaml.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SVAML) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue svaml() { + return svaml; + } + + /** Return true if this DescribeSvamlRequest object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DescribeSvamlRequestImpl describeSvamlRequest = (DescribeSvamlRequestImpl) o; + return Objects.equals(this.svaml, describeSvamlRequest.svaml); + } + + @Override + public int hashCode() { + return Objects.hash(svaml); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class DescribeSvamlRequestImpl {\n"); + sb.append(" svaml: ").append(toIndentedString(svaml)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements DescribeSvamlRequest.Builder { + OptionalValue svaml = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_SVAML, required = true) + public Builder setSvaml(SvamlInput svaml) { + this.svaml = OptionalValue.of(svaml); + return this; + } + + public DescribeSvamlRequest build() { + return new DescribeSvamlRequestImpl(svaml); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponse.java new file mode 100644 index 000000000..be1b7bc31 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponse.java @@ -0,0 +1,44 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** Response containing a human-readable description of the SVAML call flow. */ +@JsonDeserialize(builder = SvamlDescriptionResponseImpl.Builder.class) +public interface SvamlDescriptionResponse { + + /** + * Human-readable description of the call flow. + * + * @return description + */ + String getDescription(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new SvamlDescriptionResponseImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param description see getter + * @return Current builder + * @see #getDescription + */ + Builder setDescription(String description); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + SvamlDescriptionResponse build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponseImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponseImpl.java new file mode 100644 index 000000000..3f0d3e355 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponseImpl.java @@ -0,0 +1,90 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.Objects; + +@JsonPropertyOrder({SvamlDescriptionResponseImpl.JSON_PROPERTY_DESCRIPTION}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class SvamlDescriptionResponseImpl implements SvamlDescriptionResponse { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_DESCRIPTION = "description"; + + private OptionalValue description; + + public SvamlDescriptionResponseImpl() {} + + protected SvamlDescriptionResponseImpl(OptionalValue description) { + this.description = description; + } + + @JsonIgnore + public String getDescription() { + return description.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue description() { + return description; + } + + /** Return true if this SvamlDescriptionResponse object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SvamlDescriptionResponseImpl svamlDescriptionResponse = (SvamlDescriptionResponseImpl) o; + return Objects.equals(this.description, svamlDescriptionResponse.description); + } + + @Override + public int hashCode() { + return Objects.hash(description); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SvamlDescriptionResponseImpl {\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements SvamlDescriptionResponse.Builder { + OptionalValue description = OptionalValue.empty(); + + @JsonProperty(JSON_PROPERTY_DESCRIPTION) + public Builder setDescription(String description) { + this.description = OptionalValue.of(description); + return this; + } + + public SvamlDescriptionResponse build() { + return new SvamlDescriptionResponseImpl(description); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequestDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequestDtoTest.java new file mode 100644 index 000000000..8e9fc053c --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/request/DescribeSvamlRequestDtoTest.java @@ -0,0 +1,27 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.request; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInputDtoTest; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class DescribeSvamlRequestDtoTest extends BaseTest { + + public static final DescribeSvamlRequest expectedDescribeSvamlRequest = + DescribeSvamlRequest.builder().setSvaml(SvamlInputDtoTest.expectedSvamlInput).build(); + + @GivenTextResource("/domains/voice/v2/svaml/request/DescribeSvamlRequestDto.json") + String jsonDescribeSvamlRequest; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedDescribeSvamlRequest); + + JSONAssert.assertEquals(jsonDescribeSvamlRequest, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponseDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponseDtoTest.java new file mode 100644 index 000000000..ba579ec34 --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/response/SvamlDescriptionResponseDtoTest.java @@ -0,0 +1,28 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class SvamlDescriptionResponseDtoTest extends BaseTest { + + public static final SvamlDescriptionResponse expectedSvamlDescriptionResponse = + SvamlDescriptionResponse.builder() + .setDescription("The call is answered and a TTS message is played using the voice Emma.") + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/response/SvamlDescriptionResponseDto.json") + String jsonSvamlDescriptionResponse; + + @Test + void deserialize() throws JsonProcessingException { + SvamlDescriptionResponse deserialized = + objectMapper.readValue(jsonSvamlDescriptionResponse, SvamlDescriptionResponse.class); + + TestHelpers.recursiveEquals(deserialized, expectedSvamlDescriptionResponse); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/request/DescribeSvamlRequestDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/request/DescribeSvamlRequestDto.json new file mode 100644 index 000000000..cb4558478 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/request/DescribeSvamlRequestDto.json @@ -0,0 +1,19 @@ +{ + "svaml": { + "commands": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "callName": "incoming", + "events": { + "onHangup": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/response/SvamlDescriptionResponseDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/response/SvamlDescriptionResponseDto.json new file mode 100644 index 000000000..0977a210a --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/response/SvamlDescriptionResponseDto.json @@ -0,0 +1,3 @@ +{ + "description": "The call is answered and a TTS message is played using the voice Emma." +} From 708a0c07a8def7330917a98c1f9c37fc50470d7e Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Fri, 9 Oct 2026 10:27:06 +0200 Subject: [PATCH 38/50] Add Voice v2 svaml.validate endpoint --- CHANGELOG.md | 2 + .../api/v2/adapters/SvamlServiceTest.java | 64 ++++++++++ .../sdk/e2e/domains/voice/v2/SvamlSteps.java | 63 +++++---- .../sdk/e2e/domains/voice/v2/VoiceIT.java | 1 + .../snippets/src/main/java/voice/README.md | 1 + .../main/java/voice/v2/svaml/Validate.java | 66 ++++++++++ .../domains/voice/api/v2/SvamlService.java | 16 +++ .../api/v2/adapters/SvamlServiceImpl.java | 67 ++++++++++ .../svaml/request/ValidateSvamlRequest.java | 106 ++++++++++++++++ .../request/ValidateSvamlRequestImpl.java | 120 ++++++++++++++++++ .../svaml/response/ValidateSvamlResponse.java | 72 +++++++++++ .../response/ValidateSvamlResponseImpl.java | 120 ++++++++++++++++++ .../request/ValidateSvamlRequestDtoTest.java | 31 +++++ .../ValidateSvamlResponseDtoTest.java | 33 +++++ .../request/ValidateSvamlRequestDto.json | 20 +++ .../response/ValidateSvamlResponseDto.json | 7 + 16 files changed, 766 insertions(+), 23 deletions(-) create mode 100644 examples/snippets/src/main/java/voice/v2/svaml/Validate.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequest.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequestImpl.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponse.java create mode 100644 openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponseImpl.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequestDtoTest.java create mode 100644 openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponseDtoTest.java create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/request/ValidateSvamlRequestDto.json create mode 100644 openapi-contracts/src/test/resources/domains/voice/v2/svaml/response/ValidateSvamlResponseDto.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a14dc53..3055193ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Services API: `update` operation to partially update a voice service - **[feature]** Voice V2 Services API: `delete` operation to delete a voice service by its ID - **[feature]** Voice V2 Svaml API: `describe` operation to get a human-readable description of the call flow of a SVAML payload +- **[feature]** Voice V2 Svaml API: `validate` operation to check a SVAML payload against the expected schema and rules, in `NORMAL` or `STRICT` mode - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet @@ -52,6 +53,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/services/Update` snippet - **[doc]** New `voice/v2/services/Delete` snippet - **[doc]** New `voice/v2/svaml/Describe` snippet +- **[doc]** New `voice/v2/svaml/Validate` snippet - **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceTest.java index 9d9ad04d4..02b6e374f 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceTest.java @@ -21,8 +21,11 @@ import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.domains.voice.api.v2.SvamlService; import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequestDtoTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.ValidateSvamlRequestDtoTest; import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponseDtoTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.ValidateSvamlResponse; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.ValidateSvamlResponseDtoTest; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -52,6 +55,12 @@ public class SvamlServiceTest extends BaseTest { @GivenTextResource("/domains/voice/v2/svaml/response/SvamlDescriptionResponseDto.json") String jsonSvamlDescriptionResponseDto; + @GivenTextResource("/domains/voice/v2/svaml/request/ValidateSvamlRequestDto.json") + String jsonValidateSvamlRequestDto; + + @GivenTextResource("/domains/voice/v2/svaml/response/ValidateSvamlResponseDto.json") + String jsonValidateSvamlResponseDto; + @BeforeEach public void initMocks() { service = @@ -113,4 +122,59 @@ void describeMissingProjectIdThrows() { Assertions.assertEquals(400, thrown.getCode()); } + + @Test + void validate() throws ApiException { + + HttpRequest httpRequest = + new HttpRequest( + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/svaml/validate", + HttpMethod.POST, + Collections.emptyList(), + jsonValidateSvamlRequestDto, + Collections.emptyMap(), + ACCEPTS, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + AUTH_NAMES); + HttpResponse httpResponse = + new HttpResponse( + 200, null, Collections.emptyMap(), jsonValidateSvamlResponseDto.getBytes()); + + when(httpClient.invokeAPI( + eq(serverConfiguration), + eq(authManagers), + argThat(new HttpRequestMatcher(httpRequest)))) + .thenReturn(httpResponse); + + ValidateSvamlResponse response = + service.validate(ValidateSvamlRequestDtoTest.expectedValidateSvamlRequest); + + TestHelpers.recursiveEquals( + response, ValidateSvamlResponseDtoTest.expectedValidateSvamlResponse); + } + + @Test + void validateMissingRequestThrows() { + + ApiException thrown = Assertions.assertThrows(ApiException.class, () -> service.validate(null)); + + Assertions.assertEquals(400, thrown.getCode()); + } + + @Test + void validateMissingProjectIdThrows() { + + SvamlService serviceWithoutProjectId = + new SvamlServiceImpl( + httpClient, serverConfiguration, authManagers, HttpMapper.getInstance(), null); + + ApiException thrown = + Assertions.assertThrows( + ApiException.class, + () -> + serviceWithoutProjectId.validate( + ValidateSvamlRequestDtoTest.expectedValidateSvamlRequest)); + + Assertions.assertEquals(400, thrown.getCode()); + } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SvamlSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SvamlSteps.java index 4d42947ea..ecb779368 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SvamlSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/SvamlSteps.java @@ -12,7 +12,9 @@ import com.sinch.sdk.domains.voice.models.v2.svaml.playback.PlayMessage; import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage; import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.ValidateSvamlRequest; import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.ValidateSvamlResponse; import com.sinch.sdk.e2e.Config; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; @@ -24,6 +26,7 @@ public class SvamlSteps { SvamlService service; + ValidateSvamlResponse validateResponse; SvamlDescriptionResponse describeResponse; @Given("^the Voice-V2 service \"Svaml\" is available$") @@ -32,8 +35,44 @@ public void serviceAvailable() { Assertions.assertNotNull(service, "Voice V2 Svaml service is not available"); } + @When("^I send a request to validate a SVAML payload$") + public void validate() { + validateResponse = + service.validate(ValidateSvamlRequest.builder().setSvaml(svamlPayload()).build()); + } + + @Then("^the response confirms the SVAML payload is valid$") + public void validateResult() { + ValidateSvamlResponse expected = ValidateSvamlResponse.builder().setIsValid(true).build(); + + TestHelpers.recursiveEquals(validateResponse, expected); + } + @When("^I send a request to describe a SVAML payload$") public void describe() { + describeResponse = + service.describe(DescribeSvamlRequest.builder().setSvaml(svamlPayload()).build()); + } + + @Then("^the response contains the description of the SVAML payload$") + public void describeResult() { + SvamlDescriptionResponse expected = + SvamlDescriptionResponse.builder() + .setDescription( + "1. A new call with name 'audio-notification' will be initiated to number" + + " +12017777777 with max duration set to 5 minutes.\n" + + " * on answer:\n" + + " 1. An audio file will be played from" + + " https://samplelib.com/mp3/sample-12s.mp3.\n" + + " 1. A TTS message will be played using the voice Emma.\n" + + " * on finish:\n" + + " 1. The call will be disconnected") + .build(); + + TestHelpers.recursiveEquals(describeResponse, expected); + } + + private static SvamlInput svamlPayload() { MessagesCommand notification = MessagesCommand.builder() .setMessagesName("notification") @@ -65,28 +104,6 @@ public void describe() { CallEvents.builder().setOnAnswer(Collections.singletonList(notification)).build()) .build(); - describeResponse = - service.describe( - DescribeSvamlRequest.builder() - .setSvaml(SvamlInput.builder().setCommands(Collections.singletonList(dial)).build()) - .build()); - } - - @Then("^the response contains the description of the SVAML payload$") - public void describeResult() { - SvamlDescriptionResponse expected = - SvamlDescriptionResponse.builder() - .setDescription( - "1. A new call with name 'audio-notification' will be initiated to number" - + " +12017777777 with max duration set to 5 minutes.\n" - + " * on answer:\n" - + " 1. An audio file will be played from" - + " https://samplelib.com/mp3/sample-12s.mp3.\n" - + " 1. A TTS message will be played using the voice Emma.\n" - + " * on finish:\n" - + " 1. The call will be disconnected") - .build(); - - TestHelpers.recursiveEquals(describeResponse, expected); + return SvamlInput.builder().setCommands(Collections.singletonList(dial)).build(); } } diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java index d6a5867aa..800cdb2c0 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/VoiceIT.java @@ -42,6 +42,7 @@ + "|^\\[Update\\] update a Voice-V2 service$" + "|^\\[Delete\\] delete a Voice-V2 service$" + "|^\\[List\\] list (a page of|all the) Voice-V2 services.*$" + + "|^\\[Validate\\] validate a SVAML payload$" + "|^\\[Describe\\] describe a SVAML payload$" + "|^\\[(Incoming Call|Answered Call|Answer Webhook) Event\\].*") @ConfigurationParameter(key = PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, value = "true") diff --git a/examples/snippets/src/main/java/voice/README.md b/examples/snippets/src/main/java/voice/README.md index b751941c9..33981334e 100644 --- a/examples/snippets/src/main/java/voice/README.md +++ b/examples/snippets/src/main/java/voice/README.md @@ -44,3 +44,4 @@ See main [README.md](../../../../README.md) for how to execute snippets - [voice/v2/services/Update](./v2/services/Update.java) - Svaml - [voice/v2/svaml/Describe](./v2/svaml/Describe.java) + - [voice/v2/svaml/Validate](./v2/svaml/Validate.java) diff --git a/examples/snippets/src/main/java/voice/v2/svaml/Validate.java b/examples/snippets/src/main/java/voice/v2/svaml/Validate.java new file mode 100644 index 000000000..50f3279a0 --- /dev/null +++ b/examples/snippets/src/main/java/voice/v2/svaml/Validate.java @@ -0,0 +1,66 @@ +/** + * Sinch Java Snippet + * + *

This snippet is available at https://github.com/sinch/sinch-sdk-java + * + *

See https://github.com/sinch/sinch-sdk-java/blob/main/examples/snippets/README.md for details + */ +package voice.v2.svaml; + +import com.sinch.sdk.SinchClient; +import com.sinch.sdk.domains.voice.api.v2.SvamlService; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.MessagesCommand; +import com.sinch.sdk.domains.voice.models.v2.svaml.playback.SayMessage; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.ValidateSvamlRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.ValidateSvamlResponse; +import com.sinch.sdk.models.Configuration; +import java.util.Collections; +import java.util.logging.Logger; +import utils.Settings; + +public class Validate { + + private static final Logger LOGGER = Logger.getLogger(Validate.class.getName()); + + public static void main(String[] args) { + + String projectId = Settings.getProjectId().orElse("MY_PROJECT_ID"); + String keyId = Settings.getKeyId().orElse("MY_KEY_ID"); + String keySecret = Settings.getKeySecret().orElse("MY_KEY_SECRET"); + + Configuration configuration = + Configuration.builder() + .setProjectId(projectId) + .setKeyId(keyId) + .setKeySecret(keySecret) + .build(); + + SinchClient client = new SinchClient(configuration); + + SvamlService svamlService = client.voice().v2().svaml(); + + ValidateSvamlRequest request = + ValidateSvamlRequest.builder() + .setSvaml( + SvamlInput.builder() + .setCommands( + Collections.singletonList( + MessagesCommand.builder() + .setMessages( + Collections.singletonList( + SayMessage.builder() + .setText("Hello, your call is now connected.") + .setVoiceName("Emma") + .build())) + .build())) + .build()) + .build(); + + LOGGER.info("Validate a SVAML payload"); + + ValidateSvamlResponse response = svamlService.validate(request); + + LOGGER.info("Response: " + response); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SvamlService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SvamlService.java index 605e15048..023871575 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SvamlService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/SvamlService.java @@ -2,7 +2,9 @@ import com.sinch.sdk.core.exceptions.ApiException; import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.ValidateSvamlRequest; import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.ValidateSvamlResponse; /** Svaml Service */ public interface SvamlService { @@ -20,4 +22,18 @@ public interface SvamlService { * @since 2.3 */ SvamlDescriptionResponse describe(DescribeSvamlRequest describeSvamlRequest) throws ApiException; + + /** + * Validate a SVAML payload + * + *

This endpoint checks the structure and content of the SVAML commands to ensure they conform + * to the expected schema and rules. Use validationType to choose NORMAL + * (same rules as initiating a call) or STRICT (errors for unrecognised properties). + * + * @param validateSvamlRequest The SVAML payload to validate (required) + * @return ValidateSvamlResponse + * @throws ApiException if fails to make API call + * @since 2.3 + */ + ValidateSvamlResponse validate(ValidateSvamlRequest validateSvamlRequest) throws ApiException; } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceImpl.java index 0785407d5..70a9d8cd6 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/SvamlServiceImpl.java @@ -14,7 +14,9 @@ import com.sinch.sdk.core.http.URLPathUtils; import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.domains.voice.models.v2.svaml.request.DescribeSvamlRequest; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.ValidateSvamlRequest; import com.sinch.sdk.domains.voice.models.v2.svaml.response.SvamlDescriptionResponse; +import com.sinch.sdk.domains.voice.models.v2.svaml.response.ValidateSvamlResponse; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -110,4 +112,69 @@ private HttpRequest describeRequestBuilder(DescribeSvamlRequest describeSvamlReq localVarContentTypes, localVarAuthNames); } + + @Override + public ValidateSvamlResponse validate(ValidateSvamlRequest validateSvamlRequest) + throws ApiException { + + LOGGER.finest("[validate]" + " " + "validateSvamlRequest: " + validateSvamlRequest); + + HttpRequest httpRequest = validateRequestBuilder(validateSvamlRequest); + HttpResponse response = + httpClient.invokeAPI( + this.serverConfiguration, this.authManagersByOasSecuritySchemes, httpRequest); + + if (HttpStatus.isSuccessfulStatus(response.getCode())) { + return mapper.deserialize(response, new TypeReference() {}); + } + // fallback to default errors handling: + // all error cases definition are not required from specs: will try some "hardcoded" content + // parsing + throw ApiExceptionBuilder.build( + response.getMessage(), + response.getCode(), + mapper.deserialize(response, new TypeReference>() {})); + } + + private HttpRequest validateRequestBuilder(ValidateSvamlRequest validateSvamlRequest) + throws ApiException { + // verify the required parameter 'this.projectId' is set + if (this.projectId == null) { + throw new ApiException( + 400, "Missing the required parameter 'this.projectId' when calling validate"); + } + // verify the required parameter 'validateSvamlRequest' is set + if (validateSvamlRequest == null) { + throw new ApiException( + 400, "Missing the required parameter 'validateSvamlRequest' when calling validate"); + } + + String localVarPath = + "/v2/projects/{projectId}/svaml/validate" + .replaceAll( + "\\{" + "projectId" + "\\}", + URLPathUtils.encodePathSegment(this.projectId.toString())); + + List localVarQueryParams = new ArrayList<>(); + + Map localVarHeaderParams = new HashMap<>(); + + final Collection localVarAccepts = + Arrays.asList("application/json", "application/problem+json"); + + final Collection localVarContentTypes = Arrays.asList("application/json"); + + final Collection localVarAuthNames = Arrays.asList("BasicAuth", "SinchOAuth2"); + final String serializedBody = mapper.serialize(localVarContentTypes, validateSvamlRequest); + + return new HttpRequest( + localVarPath, + HttpMethod.POST, + localVarQueryParams, + serializedBody, + localVarHeaderParams, + localVarAccepts, + localVarContentTypes, + localVarAuthNames); + } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequest.java new file mode 100644 index 000000000..3bd81004c --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequest.java @@ -0,0 +1,106 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.request; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.sinch.sdk.core.utils.EnumDynamic; +import com.sinch.sdk.core.utils.EnumSupportDynamic; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Request payload for validating a SVAML document before using it in a call. + * + *

Use validationType to control how strictly the payload is checked. + */ +@JsonDeserialize(builder = ValidateSvamlRequestImpl.Builder.class) +public interface ValidateSvamlRequest { + + /** Controls how strictly the SVAML payload is validated. */ + public class ValidationTypeEnum extends EnumDynamic { + public static final ValidationTypeEnum NORMAL = new ValidationTypeEnum("NORMAL"); + + public static final ValidationTypeEnum STRICT = new ValidationTypeEnum("STRICT"); + + private static final EnumSupportDynamic ENUM_SUPPORT = + new EnumSupportDynamic<>( + ValidationTypeEnum.class, ValidationTypeEnum::new, Arrays.asList(NORMAL, STRICT)); + + private ValidationTypeEnum(String value) { + super(value); + } + + public static Stream values() { + return ENUM_SUPPORT.values(); + } + + public static ValidationTypeEnum from(String value) { + return ENUM_SUPPORT.from(value); + } + + public static String valueOf(ValidationTypeEnum e) { + return ENUM_SUPPORT.valueOf(e); + } + } + + /** + * The SVAML payload to validate. + * + *

Field is required + * + * @return svaml + */ + SvamlInput getSvaml(); + + /** + * Controls how strictly the SVAML payload is validated. + * + *

NORMAL: applies the same validation rules used when initiating a call. Unknown + * or extra properties are ignored. + * + *

STRICT: applies stricter validation rules. Returns errors for any properties + * that are not recognised or not expected in the given context. + * + *

If omitted, the server applies NORMAL. + * + * @return validationType + */ + ValidationTypeEnum getValidationType(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new ValidateSvamlRequestImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param svaml see getter + * @return Current builder + * @see #getSvaml + */ + Builder setSvaml(SvamlInput svaml); + + /** + * see getter + * + * @param validationType see getter + * @return Current builder + * @see #getValidationType + */ + Builder setValidationType(ValidationTypeEnum validationType); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + ValidateSvamlRequest build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequestImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequestImpl.java new file mode 100644 index 000000000..249dff845 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequestImpl.java @@ -0,0 +1,120 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.request; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInput; +import java.util.Objects; + +@JsonPropertyOrder({ + ValidateSvamlRequestImpl.JSON_PROPERTY_SVAML, + ValidateSvamlRequestImpl.JSON_PROPERTY_VALIDATION_TYPE +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class ValidateSvamlRequestImpl implements ValidateSvamlRequest { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_SVAML = "svaml"; + + private OptionalValue svaml; + + public static final String JSON_PROPERTY_VALIDATION_TYPE = "validationType"; + + private OptionalValue validationType; + + public ValidateSvamlRequestImpl() {} + + protected ValidateSvamlRequestImpl( + OptionalValue svaml, OptionalValue validationType) { + this.svaml = svaml; + this.validationType = validationType; + } + + @JsonIgnore + public SvamlInput getSvaml() { + return svaml.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SVAML) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue svaml() { + return svaml; + } + + @JsonIgnore + public ValidationTypeEnum getValidationType() { + return validationType.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_VALIDATION_TYPE) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue validationType() { + return validationType; + } + + /** Return true if this ValidateSvamlRequest object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValidateSvamlRequestImpl validateSvamlRequest = (ValidateSvamlRequestImpl) o; + return Objects.equals(this.svaml, validateSvamlRequest.svaml) + && Objects.equals(this.validationType, validateSvamlRequest.validationType); + } + + @Override + public int hashCode() { + return Objects.hash(svaml, validationType); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ValidateSvamlRequestImpl {\n"); + sb.append(" svaml: ").append(toIndentedString(svaml)).append("\n"); + sb.append(" validationType: ").append(toIndentedString(validationType)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements ValidateSvamlRequest.Builder { + OptionalValue svaml = OptionalValue.empty(); + OptionalValue validationType = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_SVAML, required = true) + public Builder setSvaml(SvamlInput svaml) { + this.svaml = OptionalValue.of(svaml); + return this; + } + + @JsonProperty(JSON_PROPERTY_VALIDATION_TYPE) + public Builder setValidationType(ValidationTypeEnum validationType) { + this.validationType = OptionalValue.of(validationType); + return this; + } + + public ValidateSvamlRequest build() { + return new ValidateSvamlRequestImpl(svaml, validationType); + } + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponse.java new file mode 100644 index 000000000..608afda37 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponse.java @@ -0,0 +1,72 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.response; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; + +/** + * Result of a SVAML validation request. + * + *

The HTTP 200 status indicates the validation was successfully performed — not + * that the payload is valid. Inspect isValid to determine the outcome. + */ +@JsonDeserialize(builder = ValidateSvamlResponseImpl.Builder.class) +public interface ValidateSvamlResponse { + + /** + * true if the submitted SVAML payload passed validation; false if one + * or more errors were found. + * + *

Field is required + * + * @return isValid + */ + Boolean getIsValid(); + + /** + * Validation error messages describing why the SVAML payload is invalid. + * + *

Present only when isValid is false. Each entry identifies a + * specific problem, including the affected field or command where applicable. + * + * @return errors + */ + List getErrors(); + + /** + * Getting builder + * + * @return New Builder instance + */ + static Builder builder() { + return new ValidateSvamlResponseImpl.Builder(); + } + + /** Dedicated Builder */ + interface Builder { + + /** + * see getter + * + * @param isValid see getter + * @return Current builder + * @see #getIsValid + */ + Builder setIsValid(Boolean isValid); + + /** + * see getter + * + * @param errors see getter + * @return Current builder + * @see #getErrors + */ + Builder setErrors(List errors); + + /** + * Create instance + * + * @return The instance build with current builder values + */ + ValidateSvamlResponse build(); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponseImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponseImpl.java new file mode 100644 index 000000000..613125ce4 --- /dev/null +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponseImpl.java @@ -0,0 +1,120 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.response; + +import com.fasterxml.jackson.annotation.JsonFilter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.sinch.sdk.core.models.OptionalValue; +import java.util.List; +import java.util.Objects; + +@JsonPropertyOrder({ + ValidateSvamlResponseImpl.JSON_PROPERTY_IS_VALID, + ValidateSvamlResponseImpl.JSON_PROPERTY_ERRORS +}) +@JsonFilter("uninitializedFilter") +@JsonInclude(value = JsonInclude.Include.CUSTOM) +public class ValidateSvamlResponseImpl implements ValidateSvamlResponse { + private static final long serialVersionUID = 1L; + + public static final String JSON_PROPERTY_IS_VALID = "isValid"; + + private OptionalValue isValid; + + public static final String JSON_PROPERTY_ERRORS = "errors"; + + private OptionalValue> errors; + + public ValidateSvamlResponseImpl() {} + + protected ValidateSvamlResponseImpl( + OptionalValue isValid, OptionalValue> errors) { + this.isValid = isValid; + this.errors = errors; + } + + @JsonIgnore + public Boolean getIsValid() { + return isValid.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_IS_VALID) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue isValid() { + return isValid; + } + + @JsonIgnore + public List getErrors() { + return errors.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_ERRORS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> errors() { + return errors; + } + + /** Return true if this ValidateSvamlResponse object is equal to o. */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValidateSvamlResponseImpl validateSvamlResponse = (ValidateSvamlResponseImpl) o; + return Objects.equals(this.isValid, validateSvamlResponse.isValid) + && Objects.equals(this.errors, validateSvamlResponse.errors); + } + + @Override + public int hashCode() { + return Objects.hash(isValid, errors); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ValidateSvamlResponseImpl {\n"); + sb.append(" isValid: ").append(toIndentedString(isValid)).append("\n"); + sb.append(" errors: ").append(toIndentedString(errors)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + @JsonPOJOBuilder(withPrefix = "set") + static class Builder implements ValidateSvamlResponse.Builder { + OptionalValue isValid = OptionalValue.empty(); + OptionalValue> errors = OptionalValue.empty(); + + @JsonProperty(value = JSON_PROPERTY_IS_VALID, required = true) + public Builder setIsValid(Boolean isValid) { + this.isValid = OptionalValue.of(isValid); + return this; + } + + @JsonProperty(JSON_PROPERTY_ERRORS) + public Builder setErrors(List errors) { + this.errors = OptionalValue.of(errors); + return this; + } + + public ValidateSvamlResponse build() { + return new ValidateSvamlResponseImpl(isValid, errors); + } + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequestDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequestDtoTest.java new file mode 100644 index 000000000..48446499a --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/request/ValidateSvamlRequestDtoTest.java @@ -0,0 +1,31 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.request; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.SvamlInputDtoTest; +import com.sinch.sdk.domains.voice.models.v2.svaml.request.ValidateSvamlRequest.ValidationTypeEnum; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +@TestWithResources +public class ValidateSvamlRequestDtoTest extends BaseTest { + + public static final ValidateSvamlRequest expectedValidateSvamlRequest = + ValidateSvamlRequest.builder() + .setSvaml(SvamlInputDtoTest.expectedSvamlInput) + .setValidationType(ValidationTypeEnum.STRICT) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/request/ValidateSvamlRequestDto.json") + String jsonValidateSvamlRequest; + + @Test + void serialize() throws JsonProcessingException, JSONException { + String serializedString = objectMapper.writeValueAsString(expectedValidateSvamlRequest); + + JSONAssert.assertEquals(jsonValidateSvamlRequest, serializedString, true); + } +} diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponseDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponseDtoTest.java new file mode 100644 index 000000000..5d2429d2e --- /dev/null +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/response/ValidateSvamlResponseDtoTest.java @@ -0,0 +1,33 @@ +package com.sinch.sdk.domains.voice.models.v2.svaml.response; + +import com.adelean.inject.resources.junit.jupiter.GivenTextResource; +import com.adelean.inject.resources.junit.jupiter.TestWithResources; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.sinch.sdk.BaseTest; +import com.sinch.sdk.core.TestHelpers; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +@TestWithResources +public class ValidateSvamlResponseDtoTest extends BaseTest { + + public static final ValidateSvamlResponse expectedValidateSvamlResponse = + ValidateSvamlResponse.builder() + .setIsValid(false) + .setErrors( + Arrays.asList( + "Invalid dial command. Missing destination.", + "Invalid say command. Missing voice name.")) + .build(); + + @GivenTextResource("/domains/voice/v2/svaml/response/ValidateSvamlResponseDto.json") + String jsonValidateSvamlResponse; + + @Test + void deserialize() throws JsonProcessingException { + ValidateSvamlResponse deserialized = + objectMapper.readValue(jsonValidateSvamlResponse, ValidateSvamlResponse.class); + + TestHelpers.recursiveEquals(deserialized, expectedValidateSvamlResponse); + } +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/request/ValidateSvamlRequestDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/request/ValidateSvamlRequestDto.json new file mode 100644 index 000000000..65895b3bb --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/request/ValidateSvamlRequestDto.json @@ -0,0 +1,20 @@ +{ + "svaml": { + "commands": [ + { + "command": "hangup", + "callName": "origin" + } + ], + "callName": "incoming", + "events": { + "onHangup": [ + { + "command": "hangup", + "callName": "origin" + } + ] + } + }, + "validationType": "STRICT" +} diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/response/ValidateSvamlResponseDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/response/ValidateSvamlResponseDto.json new file mode 100644 index 000000000..fd3e01e68 --- /dev/null +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/response/ValidateSvamlResponseDto.json @@ -0,0 +1,7 @@ +{ + "isValid": false, + "errors": [ + "Invalid dial command. Missing destination.", + "Invalid say command. Missing voice name." + ] +} From 499a2ecc109f22eef23d08af12e1c3f040db94a9 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Fri, 9 Oct 2026 14:59:53 +0200 Subject: [PATCH 39/50] DEVEXP-1630: Generate the Idempotency-Key in the transport --- CHANGELOG.md | 3 + .../com/sinch/sdk/http/HttpClientApache.java | 17 ++- .../api/v2/adapters/BatchesServiceTest.java | 3 +- .../api/v2/adapters/CallsServiceTest.java | 9 +- .../api/v2/adapters/ServicesServiceTest.java | 16 ++- .../HttpClientApacheIdempotencyKeyTest.java | 132 ++++++++++++++++++ .../com/sinch/sdk/core/http/HttpRequest.java | 42 ++++++ .../sinch/sdk/core/http/IdempotencyKey.java | 33 +++++ .../sdk/core/http/IdempotencyKeyTest.java | 19 +++ .../api/v2/adapters/BatchesServiceImpl.java | 3 +- .../api/v2/adapters/CallsServiceImpl.java | 9 +- .../api/v2/adapters/ServicesServiceImpl.java | 6 +- .../v2/batches/request/StartBatchRequest.java | 4 +- .../v2/calls/request/CallPatchRequest.java | 4 +- .../v2/calls/request/StartCallRequest.java | 4 +- .../request/CreateServiceRequest.java | 4 +- .../request/UpdateServiceRequest.java | 4 +- 17 files changed, 290 insertions(+), 22 deletions(-) create mode 100644 client/src/test/java/com/sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java create mode 100644 core/src/main/com/sinch/sdk/core/http/IdempotencyKey.java create mode 100644 core/src/test/java/com/sinch/sdk/core/http/IdempotencyKeyTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4922268..95c6c93a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,9 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[doc]** New `voice/v2/services/Delete` snippet - **[doc]** `sinch-events` example: new Voice V2 Sinch Events controller on `/VoiceV2Event` +### SDK +- **[feature]** `Idempotency-Key` support: for the operations that accept the header, the SDK generates a key when none is set and sends the same key on the `HTTP 429` retries of a call + ## v2.2.0 - 2026-09-17 diff --git a/client/src/main/com/sinch/sdk/http/HttpClientApache.java b/client/src/main/com/sinch/sdk/http/HttpClientApache.java index bc3d7dc73..5b60f0673 100644 --- a/client/src/main/com/sinch/sdk/http/HttpClientApache.java +++ b/client/src/main/com/sinch/sdk/http/HttpClientApache.java @@ -13,6 +13,7 @@ import com.sinch.sdk.core.http.HttpRequest; import com.sinch.sdk.core.http.HttpResponse; import com.sinch.sdk.core.http.HttpStatus; +import com.sinch.sdk.core.http.IdempotencyKey; import com.sinch.sdk.core.http.URLParameter; import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.core.utils.Pair; @@ -184,7 +185,7 @@ public HttpResponse invokeAPI( String body = httpRequest.getBody(); Map formParams = httpRequest.getFormParams(); - Map headerParams = httpRequest.getHeaderParams(); + Map headerParams = withIdempotencyKey(httpRequest); Collection accept = httpRequest.getAccept(); Collection contentType = httpRequest.getContentType(); Collection authNames = httpRequest.getAuthNames(); @@ -388,6 +389,20 @@ private void addCollectionHeader( } } + // The key is added once, before the request goes through the retry policy, so that every attempt + // of the request sends the same key + private static Map withIdempotencyKey(HttpRequest httpRequest) { + Map headers = httpRequest.getHeaderParams(); + if (!httpRequest.isIdempotent() + || (null != headers + && headers.keySet().stream().anyMatch(IdempotencyKey.HEADER::equalsIgnoreCase))) { + return headers; + } + Map withKey = null == headers ? new HashMap<>() : new HashMap<>(headers); + withKey.put(IdempotencyKey.HEADER, IdempotencyKey.generate()); + return withKey; + } + private void addHeaders(ClassicRequestBuilder requestBuilder, Map headers) { if (null == headers) { diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java index 2fdaeca45..e90141279 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java @@ -93,7 +93,8 @@ void start() throws ApiException { StartBatchRequestDtoTest.expectedStartBatchRequest.getIdempotencyKey()), ACCEPTS, Collections.singletonList(HttpContentType.APPLICATION_JSON), - AUTH_NAMES); + AUTH_NAMES, + true); HttpResponse httpResponse = new HttpResponse(201, null, Collections.emptyMap(), jsonStartBatchResponseDto.getBytes()); diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java index ec0b94fb5..02c881caf 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceTest.java @@ -104,7 +104,8 @@ void start() throws ApiException { StartCallRequestDtoTest.expectedStartCallRequest.getIdempotencyKey()), ACCEPTS, Collections.singletonList(HttpContentType.APPLICATION_JSON), - AUTH_NAMES); + AUTH_NAMES, + true); HttpResponse httpResponse = new HttpResponse(201, null, Collections.emptyMap(), jsonStartCallResponseDto.getBytes()); @@ -346,7 +347,8 @@ void interactByCallId() throws ApiException { CallPatchRequestDtoTest.expectedCallPatchRequest.getIdempotencyKey()), ACCEPTS, Collections.singletonList(HttpContentType.APPLICATION_JSON), - AUTH_NAMES); + AUTH_NAMES, + true); HttpResponse httpResponse = new HttpResponse(202, null, Collections.emptyMap(), null); when(httpClient.invokeAPI( @@ -424,7 +426,8 @@ void interactByCallName() throws ApiException { CallPatchRequestDtoTest.expectedCallPatchRequest.getIdempotencyKey()), ACCEPTS, Collections.singletonList(HttpContentType.APPLICATION_JSON), - AUTH_NAMES); + AUTH_NAMES, + true); HttpResponse httpResponse = new HttpResponse(202, null, Collections.emptyMap(), null); when(httpClient.invokeAPI( diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java index 48f074907..791500d8d 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceTest.java @@ -96,7 +96,8 @@ void create() throws ApiException { CreateServiceRequestDtoTest.expectedCreateServiceRequest.getIdempotencyKey()), ACCEPTS, Collections.singletonList(HttpContentType.APPLICATION_JSON), - AUTH_NAMES); + AUTH_NAMES, + true); HttpResponse httpResponse = new HttpResponse(201, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); @@ -113,7 +114,7 @@ void create() throws ApiException { } @Test - void createWithoutIdempotencyKeySendsNoHeader() throws ApiException { + void createWithoutIdempotencyKeyLeavesItToTransport() throws ApiException { HttpRequest httpRequest = new HttpRequest( @@ -124,7 +125,8 @@ void createWithoutIdempotencyKeySendsNoHeader() throws ApiException { Collections.emptyMap(), ACCEPTS, Collections.singletonList(HttpContentType.APPLICATION_JSON), - AUTH_NAMES); + AUTH_NAMES, + true); HttpResponse httpResponse = new HttpResponse(201, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); @@ -274,7 +276,8 @@ void update() throws ApiException { UpdateServiceRequestDtoTest.expectedUpdateServiceRequest.getIdempotencyKey()), ACCEPTS, Collections.singletonList(HttpContentType.APPLICATION_JSON), - AUTH_NAMES); + AUTH_NAMES, + true); HttpResponse httpResponse = new HttpResponse(200, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); @@ -293,7 +296,7 @@ void update() throws ApiException { } @Test - void updateWithoutIdempotencyKeySendsNoHeader() throws ApiException { + void updateWithoutIdempotencyKeyLeavesItToTransport() throws ApiException { HttpRequest httpRequest = new HttpRequest( @@ -307,7 +310,8 @@ void updateWithoutIdempotencyKeySendsNoHeader() throws ApiException { Collections.emptyMap(), ACCEPTS, Collections.singletonList(HttpContentType.APPLICATION_JSON), - AUTH_NAMES); + AUTH_NAMES, + true); HttpResponse httpResponse = new HttpResponse(200, null, Collections.emptyMap(), jsonServiceResponseDto.getBytes()); diff --git a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java new file mode 100644 index 000000000..22a480d54 --- /dev/null +++ b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java @@ -0,0 +1,132 @@ +package com.sinch.sdk.http; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.sinch.sdk.core.http.HttpContentType; +import com.sinch.sdk.core.http.HttpMethod; +import com.sinch.sdk.core.http.HttpRequest; +import com.sinch.sdk.core.http.IdempotencyKey; +import com.sinch.sdk.core.models.ServerConfiguration; +import com.sinch.sdk.models.RetryConfiguration; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * The transport generates the Idempotency-Key of the operations that accept one, so + * that the adapters only forward the key set by the user. + */ +class HttpClientApacheIdempotencyKeyTest { + + private MockWebServer server; + private HttpClientApache client; + + @BeforeEach + void setUp() throws IOException { + server = new MockWebServer(); + server.start(); + client = + new HttpClientApache( + null, + new DefaultRetryManager(RetryConfiguration.builder().setMaxRetryCount(1).build())); + } + + @AfterEach + void tearDown() throws Exception { + client.close(); + server.shutdown(); + } + + @Test + void generatesKeyWhenNoneIsSet() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + + invoke(Collections.emptyMap(), true); + + assertNotNull(server.takeRequest().getHeader(IdempotencyKey.HEADER)); + } + + @Test + void keepsKeySetByUser() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + + invoke(Collections.singletonMap(IdempotencyKey.HEADER, "user-key-0123456789"), true); + + assertEquals("user-key-0123456789", server.takeRequest().getHeader(IdempotencyKey.HEADER)); + } + + // Header names are case-insensitive: a key set in lower case must not get a second, generated one + @Test + void keepsKeySetByUserInLowerCase() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + + invoke(Collections.singletonMap("idempotency-key", "user-key-0123456789"), true); + + RecordedRequest request = server.takeRequest(); + assertEquals(1, request.getHeaders().values(IdempotencyKey.HEADER).size()); + assertEquals("user-key-0123456789", request.getHeader(IdempotencyKey.HEADER)); + } + + @Test + void noKeyWhenOperationDoesNotAcceptOne() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + + invoke(Collections.emptyMap(), false); + + assertNull(server.takeRequest().getHeader(IdempotencyKey.HEADER)); + } + + // The key identifies the operation: the server can only answer a retry with the cached response + // of the first attempt if both carry the same key + @Test + void rateLimitedRetrySendsSameKey() throws Exception { + server.enqueue(new MockResponse().setResponseCode(429).addHeader("Retry-After", "0")); + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + + invoke(Collections.emptyMap(), true); + + assertEquals(2, server.getRequestCount(), "expected the 429 to be retried once"); + RecordedRequest first = server.takeRequest(); + RecordedRequest second = server.takeRequest(); + assertNotNull(first.getHeader(IdempotencyKey.HEADER)); + assertEquals(first.getHeader(IdempotencyKey.HEADER), second.getHeader(IdempotencyKey.HEADER)); + } + + @Test + void eachCallGetsItsOwnKey() throws Exception { + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + + invoke(Collections.emptyMap(), true); + invoke(Collections.emptyMap(), true); + + assertNotEquals( + server.takeRequest().getHeader(IdempotencyKey.HEADER), + server.takeRequest().getHeader(IdempotencyKey.HEADER)); + } + + private void invoke(Map headers, boolean idempotent) { + client.invokeAPI( + new ServerConfiguration(server.url("/").toString()), + Collections.emptyMap(), + new HttpRequest( + "", + HttpMethod.POST, + Collections.emptyList(), + "{}", + headers, + Collections.singletonList(HttpContentType.APPLICATION_JSON), + Collections.singletonList(HttpContentType.APPLICATION_JSON), + Collections.emptyList(), + idempotent)); + } +} diff --git a/core/src/main/com/sinch/sdk/core/http/HttpRequest.java b/core/src/main/com/sinch/sdk/core/http/HttpRequest.java index 64f692658..17fa40cce 100644 --- a/core/src/main/com/sinch/sdk/core/http/HttpRequest.java +++ b/core/src/main/com/sinch/sdk/core/http/HttpRequest.java @@ -16,6 +16,7 @@ public class HttpRequest { private final Collection contentType; private final Collection authNames; private final Map formParams; + private final boolean idempotent; public HttpRequest( String fullUrl, @@ -35,6 +36,7 @@ public HttpRequest( this.contentType = contentType; this.authNames = authNames; this.formParams = null; + this.idempotent = false; } public HttpRequest( @@ -46,6 +48,34 @@ public HttpRequest( Collection accept, Collection contentType, Collection authNames) { + this(path, method, queryParameters, body, headerParams, accept, contentType, authNames, false); + } + + /** + * Request to an operation that accepts an Idempotency-Key header + * + * @param path Path of the operation + * @param method HTTP method + * @param queryParameters Query parameters + * @param body Serialized body + * @param headerParams Headers, with the key set by the user if any + * @param accept Accepted content types + * @param contentType Content type of the body + * @param authNames Authentication schemes + * @param idempotent true when the operation accepts an Idempotency-Key + * header: the transport then generates one if the user did not set it + * @since 2.3 + */ + public HttpRequest( + String path, + HttpMethod method, + Collection queryParameters, + String body, + Map headerParams, + Collection accept, + Collection contentType, + Collection authNames, + boolean idempotent) { this.fullUrl = null; this.path = path; this.method = method; @@ -56,6 +86,7 @@ public HttpRequest( this.contentType = contentType; this.authNames = authNames; this.formParams = null; + this.idempotent = idempotent; } public HttpRequest( @@ -77,6 +108,7 @@ public HttpRequest( this.contentType = contentType; this.authNames = authNames; this.body = null; + this.idempotent = false; } public Optional getFullUrl() { @@ -118,4 +150,14 @@ public Collection getAuthNames() { public Map getFormParams() { return formParams; } + + /** + * Whether the operation accepts an Idempotency-Key header + * + * @return true when the transport has to generate a key if none is set + * @since 2.3 + */ + public boolean isIdempotent() { + return idempotent; + } } diff --git a/core/src/main/com/sinch/sdk/core/http/IdempotencyKey.java b/core/src/main/com/sinch/sdk/core/http/IdempotencyKey.java new file mode 100644 index 000000000..aba657399 --- /dev/null +++ b/core/src/main/com/sinch/sdk/core/http/IdempotencyKey.java @@ -0,0 +1,33 @@ +package com.sinch.sdk.core.http; + +import java.util.UUID; +import java.util.logging.Logger; + +/** + * Keys for the operations that accept an Idempotency-Key header. + * + *

A key identifies one operation: it is sent again on the retries of that operation and never + * reused for another one. + * + * @since 2.3 + */ +public final class IdempotencyKey { + + private static final Logger LOGGER = Logger.getLogger(IdempotencyKey.class.getName()); + + /** Name of the HTTP header carrying the key */ + public static final String HEADER = "Idempotency-Key"; + + private IdempotencyKey() {} + + /** + * Generate a new key: a random UUID, as recommended by the Sinch API standards. + * + * @return A new key, to use for one operation and its retries + */ + public static String generate() { + String key = UUID.randomUUID().toString(); + LOGGER.fine("Generated idempotency key: " + key); + return key; + } +} diff --git a/core/src/test/java/com/sinch/sdk/core/http/IdempotencyKeyTest.java b/core/src/test/java/com/sinch/sdk/core/http/IdempotencyKeyTest.java new file mode 100644 index 000000000..20f3782dd --- /dev/null +++ b/core/src/test/java/com/sinch/sdk/core/http/IdempotencyKeyTest.java @@ -0,0 +1,19 @@ +package com.sinch.sdk.core.http; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class IdempotencyKeyTest { + + // Sinch API standards: keys are 16 to 128 characters long + @Test + void generatedKeyLengthFollowsStandard() { + Assertions.assertThat(IdempotencyKey.generate()).hasSizeBetween(16, 128); + } + + // One key per operation: two calls must not return the same key + @Test + void generatedKeysAreDistinct() { + Assertions.assertThat(IdempotencyKey.generate()).isNotEqualTo(IdempotencyKey.generate()); + } +} diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java index 3285b2233..0a12c0c28 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java @@ -139,7 +139,8 @@ private HttpRequest startRequestBuilder( localVarHeaderParams, localVarAccepts, localVarContentTypes, - localVarAuthNames); + localVarAuthNames, + true); } @Override diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java index d9aa4dcd5..83c838c51 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/CallsServiceImpl.java @@ -144,7 +144,8 @@ private HttpRequest startRequestBuilder( localVarHeaderParams, localVarAccepts, localVarContentTypes, - localVarAuthNames); + localVarAuthNames, + true); } @Override @@ -434,7 +435,8 @@ private HttpRequest interactByCallIdRequestBuilder( localVarHeaderParams, localVarAccepts, localVarContentTypes, - localVarAuthNames); + localVarAuthNames, + true); } @Override @@ -526,6 +528,7 @@ private HttpRequest interactByCallNameRequestBuilder( localVarHeaderParams, localVarAccepts, localVarContentTypes, - localVarAuthNames); + localVarAuthNames, + true); } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java index a102a5647..ef69e6bb2 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/ServicesServiceImpl.java @@ -116,7 +116,8 @@ private HttpRequest createRequestBuilder(CreateServiceRequest createServiceReque localVarHeaderParams, localVarAccepts, localVarContentTypes, - localVarAuthNames); + localVarAuthNames, + true); } @Override @@ -426,6 +427,7 @@ private HttpRequest updateRequestBuilder( localVarHeaderParams, localVarAccepts, localVarContentTypes, - localVarAuthNames); + localVarAuthNames, + true); } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java index 585f28705..e3b9ac391 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/request/StartBatchRequest.java @@ -61,7 +61,9 @@ public interface StartBatchRequest { * minutes, the server returns the cached response from the original request. Using a random UUID * (v4) is strongly recommended. * - *

Sent as the Idempotency-Key header. + *

Sent as the Idempotency-Key header. If not set, the SDK generates one for the + * call and sends it again on the retries of that call. Set your own key if you need to retry the + * call yourself, since the generated one is not returned. * * @return idempotencyKey */ diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java index c3d79c3c3..3bd36b73a 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/CallPatchRequest.java @@ -35,7 +35,9 @@ public interface CallPatchRequest { * minutes, the server returns the cached response from the original request. Using a random UUID * (v4) is strongly recommended. * - *

Sent as the Idempotency-Key header. + *

Sent as the Idempotency-Key header. If not set, the SDK generates one for the + * call and sends it again on the retries of that call. Set your own key if you need to retry the + * call yourself, since the generated one is not returned. * * @return idempotencyKey */ diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java index 1a43a8c53..9cfcfd75a 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/calls/request/StartCallRequest.java @@ -35,7 +35,9 @@ public interface StartCallRequest { * minutes, the server returns the cached response from the original request. Using a random UUID * (v4) is strongly recommended. * - *

Sent as the Idempotency-Key header. + *

Sent as the Idempotency-Key header. If not set, the SDK generates one for the + * call and sends it again on the retries of that call. Set your own key if you need to retry the + * call yourself, since the generated one is not returned. * * @return idempotencyKey */ diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequest.java index f20b9e9c8..04c31785a 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequest.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/CreateServiceRequest.java @@ -44,7 +44,9 @@ public interface CreateServiceRequest { * minutes, the server returns the cached response from the original request. Using a random UUID * (v4) is strongly recommended. * - *

Sent as the Idempotency-Key header. + *

Sent as the Idempotency-Key header. If not set, the SDK generates one for the + * call and sends it again on the retries of that call. Set your own key if you need to retry the + * call yourself, since the generated one is not returned. * * @return idempotencyKey */ diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java index 141de1ffe..503118bc5 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/request/UpdateServiceRequest.java @@ -49,7 +49,9 @@ public interface UpdateServiceRequest { * minutes, the server returns the cached response from the original request. Using a random UUID * (v4) is strongly recommended. * - *

Sent as the Idempotency-Key header. + *

Sent as the Idempotency-Key header. If not set, the SDK generates one for the + * call and sends it again on the retries of that call. Set your own key if you need to retry the + * call yourself, since the generated one is not returned. * * @return idempotencyKey */ From 3fd3764d774f8596f156ee0ec27c2c856291e060 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Wed, 14 Oct 2026 16:15:07 +0200 Subject: [PATCH 40/50] DEVEXP-1707: Align Voice V2 with the batches endpoint split and menu onFailure --- CHANGELOG.md | 2 + .../api/v2/adapters/BatchesServiceTest.java | 2 +- .../e2e/domains/voice/v2/BatchesSteps.java | 2 + .../e2e/domains/voice/v2/ServicesSteps.java | 3 ++ .../domains/voice/api/v2/BatchesService.java | 34 ++++++++------ .../domains/voice/api/v2/CallsService.java | 8 ++-- .../api/v2/adapters/BatchesServiceImpl.java | 2 +- .../v2/batches/response/BatchDetails.java | 4 +- .../batches/response/BatchSessionSummary.java | 19 ++++++++ .../response/BatchSessionSummaryImpl.java | 40 ++++++++++++++-- .../v2/batches/response/BatchSummary.java | 9 +--- .../batches/response/StartBatchResponse.java | 3 +- .../v2/services/response/ServiceResponse.java | 18 +++++++ .../response/ServiceResponseImpl.java | 47 ++++++++++++++++++- .../models/v2/sinchevents/SinchEventType.java | 9 +++- .../voice/models/v2/svaml/menu/MenuItem.java | 17 +++---- .../models/v2/svaml/menu/MenuItemImpl.java | 36 +++++++------- .../batches/response/BatchDetailsDtoTest.java | 2 + .../response/BatchSessionSummaryDtoTest.java | 2 + .../response/ServiceResponseDtoTest.java | 2 + .../models/v2/svaml/menu/MenuItemDtoTest.java | 2 +- .../batches/request/StartBatchRequestDto.json | 2 +- .../v2/batches/response/BatchDetailsDto.json | 10 +++- .../response/BatchSessionSummaryDto.json | 5 +- .../v2/calls/request/StartCallRequestDto.json | 2 +- .../services/response/ServiceResponseDto.json | 1 + .../response/ServiceResponseMinimalDto.json | 1 + .../voice/v2/svaml/calls/CallEventsDto.json | 2 +- .../voice/v2/svaml/calls/DialCommandDto.json | 2 +- .../voice/v2/svaml/menu/MenuCommandDto.json | 2 +- .../voice/v2/svaml/menu/MenuItemDto.json | 2 +- 31 files changed, 217 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a8a5636..586088fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Svaml API: `describe` operation to get a human-readable description of the call flow of a SVAML payload - **[feature]** Voice V2 Svaml API: `validate` operation to check a SVAML payload against the expected schema and rules, in `NORMAL` or `STRICT` mode - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch +- **[feature]** Voice V2 `ServiceResponse` exposes `serviceSecret`, returned by `services().create`, `services().get` and `services().update` +- **[design]** Voice V2 menu items: `onFail` is renamed to `onFailure` in `MenuItem` (`getOnFailure()` / `setOnFailure()`), following the API specification - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet - **[doc]** New `voice/v2/calls/List` snippet diff --git a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java index e90141279..f4c8d7847 100644 --- a/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java +++ b/client/src/test/java/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceTest.java @@ -82,7 +82,7 @@ void start() throws ApiException { HttpRequest httpRequest = new HttpRequest( - "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/calls", + "/v2/projects/" + URLPathUtils.encodePathSegment(PROJECT_ID) + "/batches", HttpMethod.POST, Collections.singletonList( new URLParameter( diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java index b8bdf51bc..d99462400 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/BatchesSteps.java @@ -132,10 +132,12 @@ public void getDetailsResult() { BatchSessionSummary.builder() .setId("01M144V4PE5KTSVY2AX19QC332") .setState(SessionState.IN_PROGRESS) + .setParameters(Collections.singletonMap("toNumber", "+12017777777")) .build(), BatchSessionSummary.builder() .setId("01M144V4PEH22EHB1SHMJRXBXA") .setState(SessionState.COMPLETED) + .setParameters(Collections.singletonMap("toNumber", "+12018888888")) .build())) .build(); diff --git a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java index 65749495a..dbc20e5e4 100644 --- a/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java +++ b/client/src/test/java/com/sinch/sdk/e2e/domains/voice/v2/ServicesSteps.java @@ -48,6 +48,7 @@ public void createResult() { ServiceResponse expected = ServiceResponse.builder() .setServiceId("1a2b3c4d-5e6f-4789-a012-b3c4d5e6f789") + .setServiceSecret("9+MXX5u6dky5aATA7ZfIRg==") .setProjectId("c3d4e5f6-a7b8-4901-c234-d5e6f7a8b901") .setCreateTime(Instant.parse("2026-09-16T10:44:09Z")) .setUpdateTime(Instant.parse("2026-09-16T10:44:09Z")) @@ -69,6 +70,7 @@ public void getResult() { ServiceResponse expected = ServiceResponse.builder() .setServiceId("3c4d5e6f-7a8b-4901-c234-d5e6f7a8b901") + .setServiceSecret("9+MXX5u6dky5aATA7ZfIRg==") .setProjectId("c3d4e5f6-a7b8-4901-c234-d5e6f7a8b901") .setCreateTime(Instant.parse("2026-09-16T10:47:36Z")) .setUpdateTime(Instant.parse("2026-09-16T10:47:36Z")) @@ -98,6 +100,7 @@ public void updateResult() { ServiceResponse expected = ServiceResponse.builder() .setServiceId("3c4d5e6f-7a8b-4901-c234-d5e6f7a8b901") + .setServiceSecret("9+MXX5u6dky5aATA7ZfIRg==") .setProjectId("c3d4e5f6-a7b8-4901-c234-d5e6f7a8b901") .setCreateTime(Instant.parse("2026-09-16T10:47:36Z")) .setUpdateTime(Instant.parse("2026-09-16T10:51:55Z")) diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java index c474e840b..647cc283e 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/BatchesService.java @@ -12,14 +12,12 @@ public interface BatchesService { /** - * Create and initiate a batch of outbound voice calls, associated to the project's default + * Create and initiate a batch of outbound call sessions, associated to the project's default * service * - *

One call is queued for each entry in {@link StartBatchRequest#getParameters()}. + *

One call session is queued for each entry in {@link StartBatchRequest#getParameters()}. * - *

Uses the same API operation as {@link CallsService#start}. - * - * @param startBatchRequest The batch of calls to start (required) + * @param startBatchRequest The batch of call sessions to start (required) * @return StartBatchResponse * @throws ApiException if fails to make API call * @since 2.3 @@ -27,16 +25,22 @@ public interface BatchesService { StartBatchResponse start(StartBatchRequest startBatchRequest) throws ApiException; /** - * Create and initiate a batch of outbound voice calls + * Create and initiate a batch of outbound call sessions + * + *

Create a batch of outbound call sessions associated to the project's default service or to + * the service specified by {@link StartBatchQueryParameters#getServiceId()}. * - *

Create a batch of outbound calls associated to the project's default service or to the - * service specified by {@link StartBatchQueryParameters#getServiceId()}. One call is queued for - * each entry in {@link StartBatchRequest#getParameters()}. + *

The request contains a single set of SVAML commands describing the call flow, together with + * one {@link StartBatchRequest#getParameters() parameters} entry per call session to queue. + * Placeholders in the commands (prefixed with @) are substituted with the values of + * the matching entry, so each session can be personalized without changing the command structure. * - *

Uses the same API operation as {@link CallsService#start}. + *

Queued sessions are initiated asynchronously, paced according to {@link + * StartBatchRequest#getBatchOptions()}. The IDs of the individual call sessions are not part of + * the response: retrieve them with {@link #getDetails}. * * @param queryParameter (optional) - * @param startBatchRequest The batch of calls to start (required) + * @param startBatchRequest The batch of call sessions to start (required) * @return StartBatchResponse * @throws ApiException if fails to make API call * @since 2.3 @@ -48,8 +52,8 @@ StartBatchResponse start( /** * Get a batch summary * - *

Retrieve a summary of a batch call operation, including statistics on completed, - * in-progress, queued and expired call sessions. + *

Retrieve a summary of a batch, including statistics on completed, in-progress, queued and + * expired call sessions. * * @param batchId The ID of the batch, as returned by {@link #start} (required) * @return BatchSummary @@ -61,8 +65,8 @@ StartBatchResponse start( /** * Get batch details * - *

Retrieve per-session details for a batch call operation, including the current state of each - * call session in the batch. + *

Retrieve per-session details for a batch, including the current state of each call session + * in the batch and the parameters used to initiate it. * *

Use this operation when individual session-level visibility is needed (for example, to * inspect which sessions are QUEUED, IN_PROGRESS or COMPLETED diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java index 15a22904a..1e91f9ffc 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/CallsService.java @@ -13,7 +13,7 @@ public interface CallsService { /** - * Create and initiate a new outbound voice call, associated to the project's default service + * Create and initiate a new outbound call session, associated to the project's default service * * @param startCallRequest The call to start (required) * @return StartCallResponse @@ -23,10 +23,10 @@ public interface CallsService { StartCallResponse start(StartCallRequest startCallRequest) throws ApiException; /** - * Create and initiate a new outbound voice call + * Create and initiate a new outbound call session * - *

Create a new outbound call associated to the project's default service or to the service - * specified by {@link StartCallQueryParameters#getServiceId()}. + *

Create a new outbound call session associated to the project's default service or to the + * service specified by {@link StartCallQueryParameters#getServiceId()}. * * @param queryParameter (optional) * @param startCallRequest The call to start (required) diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java index 0a12c0c28..950fe99ac 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/api/v2/adapters/BatchesServiceImpl.java @@ -103,7 +103,7 @@ private HttpRequest startRequestBuilder( } String localVarPath = - "/v2/projects/{projectId}/calls" + "/v2/projects/{projectId}/batches" .replaceAll( "\\{" + "projectId" + "\\}", URLPathUtils.encodePathSegment(this.projectId.toString())); diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetails.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetails.java index 00a316764..7e8915f59 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetails.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetails.java @@ -4,8 +4,8 @@ import java.util.List; /** - * Detailed per-session view of a batch call operation. Contains one entry for each call session in - * the batch, including the session identifier and its current execution state. + * Detailed per-session view of a batch. Contains one entry for each call session in the batch, + * including the session identifier and its current execution state. */ @JsonDeserialize(builder = BatchDetailsImpl.Builder.class) public interface BatchDetails { diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummary.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummary.java index cb57897a6..f8b1bb32c 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummary.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummary.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.sinch.sdk.domains.voice.models.v2.SessionState; +import java.util.Map; /** Identifier and current execution state of a call session within a batch. */ @JsonDeserialize(builder = BatchSessionSummaryImpl.Builder.class) @@ -25,6 +26,15 @@ public interface BatchSessionSummary { */ SessionState getState(); + /** + * Plain key-value set of parameters from the {@link + * com.sinch.sdk.domains.voice.models.v2.batches.request.StartBatchRequest#getParameters() + * parameters} entry that was used to initiate this call session. + * + * @return parameters + */ + Map getParameters(); + /** * Getting builder * @@ -55,6 +65,15 @@ interface Builder { */ Builder setState(SessionState state); + /** + * see getter + * + * @param parameters see getter + * @return Current builder + * @see #getParameters + */ + Builder setParameters(Map parameters); + /** * Create instance * diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryImpl.java index f6948032c..72e272ca8 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryImpl.java @@ -8,11 +8,13 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.sinch.sdk.core.models.OptionalValue; import com.sinch.sdk.domains.voice.models.v2.SessionState; +import java.util.Map; import java.util.Objects; @JsonPropertyOrder({ BatchSessionSummaryImpl.JSON_PROPERTY_ID, - BatchSessionSummaryImpl.JSON_PROPERTY_STATE + BatchSessionSummaryImpl.JSON_PROPERTY_STATE, + BatchSessionSummaryImpl.JSON_PROPERTY_PARAMETERS }) @JsonFilter("uninitializedFilter") @JsonInclude(value = JsonInclude.Include.CUSTOM) @@ -27,11 +29,19 @@ public class BatchSessionSummaryImpl implements BatchSessionSummary { private OptionalValue state; + public static final String JSON_PROPERTY_PARAMETERS = "parameters"; + + private OptionalValue> parameters; + public BatchSessionSummaryImpl() {} - protected BatchSessionSummaryImpl(OptionalValue id, OptionalValue state) { + protected BatchSessionSummaryImpl( + OptionalValue id, + OptionalValue state, + OptionalValue> parameters) { this.id = id; this.state = state; + this.parameters = parameters; } @JsonIgnore @@ -56,6 +66,17 @@ public OptionalValue state() { return state; } + @JsonIgnore + public Map getParameters() { + return parameters.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_PARAMETERS) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public OptionalValue> parameters() { + return parameters; + } + /** Return true if this BatchSessionSummary object is equal to o. */ @Override public boolean equals(Object o) { @@ -67,12 +88,13 @@ public boolean equals(Object o) { } BatchSessionSummaryImpl batchSessionSummary = (BatchSessionSummaryImpl) o; return Objects.equals(this.id, batchSessionSummary.id) - && Objects.equals(this.state, batchSessionSummary.state); + && Objects.equals(this.state, batchSessionSummary.state) + && Objects.equals(this.parameters, batchSessionSummary.parameters); } @Override public int hashCode() { - return Objects.hash(id, state); + return Objects.hash(id, state, parameters); } @Override @@ -81,6 +103,7 @@ public String toString() { sb.append("class BatchSessionSummaryImpl {\n"); sb.append(" id: ").append(toIndentedString(id)).append("\n"); sb.append(" state: ").append(toIndentedString(state)).append("\n"); + sb.append(" parameters: ").append(toIndentedString(parameters)).append("\n"); sb.append("}"); return sb.toString(); } @@ -99,6 +122,7 @@ private String toIndentedString(Object o) { static class Builder implements BatchSessionSummary.Builder { OptionalValue id = OptionalValue.empty(); OptionalValue state = OptionalValue.empty(); + OptionalValue> parameters = OptionalValue.empty(); @JsonProperty(JSON_PROPERTY_ID) public Builder setId(String id) { @@ -112,8 +136,14 @@ public Builder setState(SessionState state) { return this; } + @JsonProperty(JSON_PROPERTY_PARAMETERS) + public Builder setParameters(Map parameters) { + this.parameters = OptionalValue.of(parameters); + return this; + } + public BatchSessionSummary build() { - return new BatchSessionSummaryImpl(id, state); + return new BatchSessionSummaryImpl(id, state, parameters); } } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummary.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummary.java index 022c5677d..f9c8f65c4 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummary.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSummary.java @@ -3,17 +3,12 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.time.Instant; -/** - * Summary of a batch call operation, including counts of call sessions by state and overall - * progress. - */ +/** Summary of a batch, including counts of call sessions by state and overall progress. */ @JsonDeserialize(builder = BatchSummaryImpl.Builder.class) public interface BatchSummary { /** - * Unique identifier of the batch call operation (ULID). - * - *

Use this value to retrieve the batch details or stop processing the batch. + * Unique identifier of the batch (ULID). * *

Field is required * diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/StartBatchResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/StartBatchResponse.java index 89acf7aef..5d39a5cb9 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/StartBatchResponse.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/batches/response/StartBatchResponse.java @@ -5,7 +5,8 @@ /** * Response returned after starting a batch of outbound calls. * - *

Contains the ID of the created batch and of the project and service it belongs to. + *

Contains the ID of the created batch and of the project and service it belongs to. The IDs of + * the individual call sessions are not included: they are part of the batch details. */ @JsonDeserialize(builder = StartBatchResponseImpl.Builder.class) public interface StartBatchResponse { diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponse.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponse.java index 4d7085c4b..1574332f6 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponse.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponse.java @@ -19,6 +19,15 @@ public interface ServiceResponse { */ String getServiceId(); + /** + * The secret of the service used. + * + *

Field is required + * + * @return serviceSecret + */ + String getServiceSecret(); + /** * The ID of the project associated with the call. * @@ -102,6 +111,15 @@ interface Builder { */ Builder setServiceId(String serviceId); + /** + * see getter + * + * @param serviceSecret see getter + * @return Current builder + * @see #getServiceSecret + */ + Builder setServiceSecret(String serviceSecret); + /** * see getter * diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseImpl.java index 4f8a6925d..ae1ea7bb5 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseImpl.java @@ -13,6 +13,7 @@ @JsonPropertyOrder({ ServiceResponseImpl.JSON_PROPERTY_SERVICE_ID, + ServiceResponseImpl.JSON_PROPERTY_SERVICE_SECRET, ServiceResponseImpl.JSON_PROPERTY_PROJECT_ID, ServiceResponseImpl.JSON_PROPERTY_CREATE_TIME, ServiceResponseImpl.JSON_PROPERTY_UPDATE_TIME, @@ -30,6 +31,10 @@ public class ServiceResponseImpl implements ServiceResponse { private OptionalValue serviceId; + public static final String JSON_PROPERTY_SERVICE_SECRET = "serviceSecret"; + + private OptionalValue serviceSecret; + public static final String JSON_PROPERTY_PROJECT_ID = "projectId"; private OptionalValue projectId; @@ -62,6 +67,7 @@ public ServiceResponseImpl() {} protected ServiceResponseImpl( OptionalValue serviceId, + OptionalValue serviceSecret, OptionalValue projectId, OptionalValue createTime, OptionalValue updateTime, @@ -70,6 +76,7 @@ protected ServiceResponseImpl( OptionalValue isDefault, OptionalValue callBehavior) { this.serviceId = serviceId; + this.serviceSecret = serviceSecret; this.projectId = projectId; this.createTime = createTime; this.updateTime = updateTime; @@ -90,6 +97,17 @@ public OptionalValue serviceId() { return serviceId; } + @JsonIgnore + public String getServiceSecret() { + return serviceSecret.orElse(null); + } + + @JsonProperty(JSON_PROPERTY_SERVICE_SECRET) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public OptionalValue serviceSecret() { + return serviceSecret; + } + @JsonIgnore public String getProjectId() { return projectId.orElse(null); @@ -176,6 +194,7 @@ public boolean equals(Object o) { } ServiceResponseImpl serviceResponse = (ServiceResponseImpl) o; return Objects.equals(this.serviceId, serviceResponse.serviceId) + && Objects.equals(this.serviceSecret, serviceResponse.serviceSecret) && Objects.equals(this.projectId, serviceResponse.projectId) && Objects.equals(this.createTime, serviceResponse.createTime) && Objects.equals(this.updateTime, serviceResponse.updateTime) @@ -188,7 +207,15 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash( - serviceId, projectId, createTime, updateTime, name, description, isDefault, callBehavior); + serviceId, + serviceSecret, + projectId, + createTime, + updateTime, + name, + description, + isDefault, + callBehavior); } @Override @@ -196,6 +223,7 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ServiceResponseImpl {\n"); sb.append(" serviceId: ").append(toIndentedString(serviceId)).append("\n"); + sb.append(" serviceSecret: ").append(toIndentedString(serviceSecret)).append("\n"); sb.append(" projectId: ").append(toIndentedString(projectId)).append("\n"); sb.append(" createTime: ").append(toIndentedString(createTime)).append("\n"); sb.append(" updateTime: ").append(toIndentedString(updateTime)).append("\n"); @@ -220,6 +248,7 @@ private String toIndentedString(Object o) { @JsonPOJOBuilder(withPrefix = "set") static class Builder implements ServiceResponse.Builder { OptionalValue serviceId = OptionalValue.empty(); + OptionalValue serviceSecret = OptionalValue.empty(); OptionalValue projectId = OptionalValue.empty(); OptionalValue createTime = OptionalValue.empty(); OptionalValue updateTime = OptionalValue.empty(); @@ -234,6 +263,12 @@ public Builder setServiceId(String serviceId) { return this; } + @JsonProperty(value = JSON_PROPERTY_SERVICE_SECRET, required = true) + public Builder setServiceSecret(String serviceSecret) { + this.serviceSecret = OptionalValue.of(serviceSecret); + return this; + } + @JsonProperty(value = JSON_PROPERTY_PROJECT_ID, required = true) public Builder setProjectId(String projectId) { this.projectId = OptionalValue.of(projectId); @@ -278,7 +313,15 @@ public Builder setCallBehavior(CallBehavior callBehavior) { public ServiceResponse build() { return new ServiceResponseImpl( - serviceId, projectId, createTime, updateTime, name, description, isDefault, callBehavior); + serviceId, + serviceSecret, + projectId, + createTime, + updateTime, + name, + description, + isDefault, + callBehavior); } } } diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/SinchEventType.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/SinchEventType.java index 275677be0..06d303f10 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/SinchEventType.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/sinchevents/SinchEventType.java @@ -68,7 +68,14 @@ public class SinchEventType extends EnumDynamic { public static final SinchEventType CALL_RECORDING_FAILED = new SinchEventType("call.recording.failed"); - /** Triggered when a menu completes and input is received from the user or the menu fails. */ + /** + * Triggered when a menu fails to collect a matching input and no onFailure commands + * are defined for that menu. + * + *

Input that matches a menu match item does not trigger this event; a {@link + * com.sinch.sdk.domains.voice.models.v2.svaml.customevents.CustomEventCommand} within the menu + * matches can be used to send an event for a specific input. + */ public static final SinchEventType CALL_MENU = new SinchEventType("call.menu"); private static final EnumSupportDynamic ENUM_SUPPORT = diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItem.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItem.java index dfe82a56c..9c28ba335 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItem.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItem.java @@ -14,10 +14,11 @@ * and failure handling. * *

Each collected input is matched against the values in the matches property. If no - * match succeeds, the onFail commands are executed. + * match succeeds, the onFailure commands are executed. * - *

If neither matches nor onFail is defined and the service call - * behavior is set to WEBHOOK, a webhook request is sent including the collected input. + *

If no match succeeds and onFailure is not defined, a call.menu event + * including the collected input is sent to the event destination when the service call behavior is + * {@link com.sinch.sdk.domains.voice.models.v2.services.EventDestinationCallBehavior}. */ @JsonDeserialize(builder = MenuItemImpl.Builder.class) public interface MenuItem { @@ -134,9 +135,9 @@ public static String valueOf(InputMethodsEnum e) { *

This handler runs after the repeat limit is reached without any input matching a menu match * item. * - * @return onFail + * @return onFailure */ - List getOnFail(); + List getOnFailure(); /** * Getting builder @@ -234,11 +235,11 @@ interface Builder { /** * see getter * - * @param onFail see getter + * @param onFailure see getter * @return Current builder - * @see #getOnFail + * @see #getOnFailure */ - Builder setOnFail(List onFail); + Builder setOnFailure(List onFailure); /** * Create instance diff --git a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItemImpl.java b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItemImpl.java index e4f50b999..03df931a8 100644 --- a/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItemImpl.java +++ b/openapi-contracts/src/main/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItemImpl.java @@ -22,7 +22,7 @@ MenuItemImpl.JSON_PROPERTY_TERMINATING_SEQUENCE, MenuItemImpl.JSON_PROPERTY_INPUT_METHODS, MenuItemImpl.JSON_PROPERTY_MATCHES, - MenuItemImpl.JSON_PROPERTY_ON_FAIL + MenuItemImpl.JSON_PROPERTY_ON_FAILURE }) @JsonFilter("uninitializedFilter") @JsonInclude(value = JsonInclude.Include.CUSTOM) @@ -66,9 +66,9 @@ public class MenuItemImpl implements MenuItem { private OptionalValue>> matches; - public static final String JSON_PROPERTY_ON_FAIL = "onFail"; + public static final String JSON_PROPERTY_ON_FAILURE = "onFailure"; - private OptionalValue> onFail; + private OptionalValue> onFailure; public MenuItemImpl() {} @@ -82,7 +82,7 @@ protected MenuItemImpl( OptionalValue terminatingSequence, OptionalValue> inputMethods, OptionalValue>> matches, - OptionalValue> onFail) { + OptionalValue> onFailure) { this.prompt = prompt; this.repeatPrompt = repeatPrompt; this.inputTimeoutDurationSeconds = inputTimeoutDurationSeconds; @@ -92,7 +92,7 @@ protected MenuItemImpl( this.terminatingSequence = terminatingSequence; this.inputMethods = inputMethods; this.matches = matches; - this.onFail = onFail; + this.onFailure = onFailure; } @JsonIgnore @@ -195,14 +195,14 @@ public OptionalValue>> matches() { } @JsonIgnore - public List getOnFail() { - return onFail.orElse(null); + public List getOnFailure() { + return onFailure.orElse(null); } - @JsonProperty(JSON_PROPERTY_ON_FAIL) + @JsonProperty(JSON_PROPERTY_ON_FAILURE) @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) - public OptionalValue> onFail() { - return onFail; + public OptionalValue> onFailure() { + return onFailure; } /** Return true if this MenuItem object is equal to o. */ @@ -224,7 +224,7 @@ public boolean equals(Object o) { && Objects.equals(this.terminatingSequence, menuItem.terminatingSequence) && Objects.equals(this.inputMethods, menuItem.inputMethods) && Objects.equals(this.matches, menuItem.matches) - && Objects.equals(this.onFail, menuItem.onFail); + && Objects.equals(this.onFailure, menuItem.onFailure); } @Override @@ -239,7 +239,7 @@ public int hashCode() { terminatingSequence, inputMethods, matches, - onFail); + onFailure); } @Override @@ -259,7 +259,7 @@ public String toString() { .append("\n"); sb.append(" inputMethods: ").append(toIndentedString(inputMethods)).append("\n"); sb.append(" matches: ").append(toIndentedString(matches)).append("\n"); - sb.append(" onFail: ").append(toIndentedString(onFail)).append("\n"); + sb.append(" onFailure: ").append(toIndentedString(onFailure)).append("\n"); sb.append("}"); return sb.toString(); } @@ -285,7 +285,7 @@ static class Builder implements MenuItem.Builder { OptionalValue terminatingSequence = OptionalValue.empty(); OptionalValue> inputMethods = OptionalValue.empty(); OptionalValue>> matches = OptionalValue.empty(); - OptionalValue> onFail = OptionalValue.empty(); + OptionalValue> onFailure = OptionalValue.empty(); @JsonProperty(JSON_PROPERTY_PROMPT) public Builder setPrompt(MenuPrompt prompt) { @@ -341,9 +341,9 @@ public Builder setMatches(Map> matches) { return this; } - @JsonProperty(JSON_PROPERTY_ON_FAIL) - public Builder setOnFail(List onFail) { - this.onFail = OptionalValue.of(onFail); + @JsonProperty(JSON_PROPERTY_ON_FAILURE) + public Builder setOnFailure(List onFailure) { + this.onFailure = OptionalValue.of(onFailure); return this; } @@ -358,7 +358,7 @@ public MenuItem build() { terminatingSequence, inputMethods, matches, - onFail); + onFailure); } } } diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsDtoTest.java index bf4b28b9e..9243b563a 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsDtoTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchDetailsDtoTest.java @@ -7,6 +7,7 @@ import com.sinch.sdk.core.TestHelpers; import com.sinch.sdk.domains.voice.models.v2.SessionState; import java.util.Arrays; +import java.util.Collections; import org.junit.jupiter.api.Test; @TestWithResources @@ -20,6 +21,7 @@ public class BatchDetailsDtoTest extends BaseTest { BatchSessionSummary.builder() .setId("01F8Z5J4X2G9Y3J4X2G9Y3J4X5YJ") .setState(SessionState.IN_PROGRESS) + .setParameters(Collections.singletonMap("numberB", "+15559876544")) .build(), BatchSessionSummary.builder() .setId("01F8Z5J4X2G9Y3J4X2G9Y3J4X9QK") diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryDtoTest.java index e757348ef..180b314e6 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryDtoTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/batches/response/BatchSessionSummaryDtoTest.java @@ -6,6 +6,7 @@ import com.sinch.sdk.BaseTest; import com.sinch.sdk.core.TestHelpers; import com.sinch.sdk.domains.voice.models.v2.SessionState; +import java.util.Collections; import org.junit.jupiter.api.Test; @TestWithResources @@ -15,6 +16,7 @@ public class BatchSessionSummaryDtoTest extends BaseTest { BatchSessionSummary.builder() .setId("01F8Z5J4X2G9Y3J4X2G9Y3J4X2G9") .setState(SessionState.COMPLETED) + .setParameters(Collections.singletonMap("numberB", "+15559876543")) .build(); @GivenTextResource("/domains/voice/v2/batches/response/BatchSessionSummaryDto.json") diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseDtoTest.java index 464aa19b5..ee535bc36 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseDtoTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/services/response/ServiceResponseDtoTest.java @@ -15,6 +15,7 @@ public class ServiceResponseDtoTest extends BaseTest { public static final ServiceResponse expectedServiceResponse = ServiceResponse.builder() .setServiceId("6e124178-c29d-46a5-943c-5c2ae544aade") + .setServiceSecret("9+MXX5u6dky5aATA7ZfIRg==") .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") .setCreateTime(Instant.parse("2025-01-01T00:00:00Z")) .setUpdateTime(Instant.parse("2025-02-15T10:30:00Z")) @@ -27,6 +28,7 @@ public class ServiceResponseDtoTest extends BaseTest { public static final ServiceResponse expectedServiceResponseMinimal = ServiceResponse.builder() .setServiceId("7f235289-d3ae-57b6-a54d-6d3bf655bbdf") + .setServiceSecret("Jq4Rz2s8VnYkL0pT7wXc1A==") .setProjectId("5c5bf2b1-35ae-4825-ab89-457e07bb60e6") .setCreateTime(Instant.parse("2025-02-15T10:30:00Z")) .setName("Secondary Service") diff --git a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItemDtoTest.java b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItemDtoTest.java index a7a4e2992..7419a20e9 100644 --- a/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItemDtoTest.java +++ b/openapi-contracts/src/test/java/com/sinch/sdk/domains/voice/models/v2/svaml/menu/MenuItemDtoTest.java @@ -28,7 +28,7 @@ public class MenuItemDtoTest extends BaseTest { .setMatches( Collections.singletonMap( "1", Arrays.asList(GotoMenuCommandDtoTest.expectedGotoMenuCommand))) - .setOnFail(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) + .setOnFailure(Arrays.asList(HangupCommandDtoTest.expectedHangupCommand)) .build(); @GivenTextResource("/domains/voice/v2/svaml/menu/MenuItemDto.json") diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/batches/request/StartBatchRequestDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/batches/request/StartBatchRequestDto.json index 7c5958430..0e081ac5c 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/batches/request/StartBatchRequestDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/batches/request/StartBatchRequestDto.json @@ -151,7 +151,7 @@ } ] }, - "onFail": [ + "onFailure": [ { "command": "hangup", "callName": "origin" diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchDetailsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchDetailsDto.json index 9305d716b..c4fbbf73a 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchDetailsDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchDetailsDto.json @@ -2,11 +2,17 @@ "sessions": [ { "id": "01F8Z5J4X2G9Y3J4X2G9Y3J4X2G9", - "state": "COMPLETED" + "state": "COMPLETED", + "parameters": { + "numberB": "+15559876543" + } }, { "id": "01F8Z5J4X2G9Y3J4X2G9Y3J4X5YJ", - "state": "IN_PROGRESS" + "state": "IN_PROGRESS", + "parameters": { + "numberB": "+15559876544" + } }, { "id": "01F8Z5J4X2G9Y3J4X2G9Y3J4X9QK", diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSessionSummaryDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSessionSummaryDto.json index ded35743a..661aaf66e 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSessionSummaryDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/batches/response/BatchSessionSummaryDto.json @@ -1,4 +1,7 @@ { "id": "01F8Z5J4X2G9Y3J4X2G9Y3J4X2G9", - "state": "COMPLETED" + "state": "COMPLETED", + "parameters": { + "numberB": "+15559876543" + } } diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/StartCallRequestDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/StartCallRequestDto.json index 8f590b4bf..0de2dd010 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/StartCallRequestDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/calls/request/StartCallRequestDto.json @@ -151,7 +151,7 @@ } ] }, - "onFail": [ + "onFailure": [ { "command": "hangup", "callName": "origin" diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseDto.json index 867b26afd..427eccdd6 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseDto.json @@ -1,5 +1,6 @@ { "serviceId": "6e124178-c29d-46a5-943c-5c2ae544aade", + "serviceSecret": "9+MXX5u6dky5aATA7ZfIRg==", "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", "createTime": "2025-01-01T00:00:00Z", "updateTime": "2025-02-15T10:30:00Z", diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseMinimalDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseMinimalDto.json index 913100347..47d2b5a0b 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseMinimalDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/services/response/ServiceResponseMinimalDto.json @@ -1,5 +1,6 @@ { "serviceId": "7f235289-d3ae-57b6-a54d-6d3bf655bbdf", + "serviceSecret": "Jq4Rz2s8VnYkL0pT7wXc1A==", "projectId": "5c5bf2b1-35ae-4825-ab89-457e07bb60e6", "createTime": "2025-02-15T10:30:00Z", "name": "Secondary Service", diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/calls/CallEventsDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/calls/CallEventsDto.json index caec50db7..abff109e2 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/calls/CallEventsDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/calls/CallEventsDto.json @@ -132,7 +132,7 @@ } ] }, - "onFail": [ + "onFailure": [ { "command": "hangup", "callName": "origin" diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/calls/DialCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/calls/DialCommandDto.json index bb82c7c69..6f6986902 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/calls/DialCommandDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/calls/DialCommandDto.json @@ -149,7 +149,7 @@ } ] }, - "onFail": [ + "onFailure": [ { "command": "hangup", "callName": "origin" diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/menu/MenuCommandDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/menu/MenuCommandDto.json index 4149f6fda..0dd79ff2b 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/menu/MenuCommandDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/menu/MenuCommandDto.json @@ -45,7 +45,7 @@ } ] }, - "onFail": [ + "onFailure": [ { "command": "hangup", "callName": "origin" diff --git a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/menu/MenuItemDto.json b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/menu/MenuItemDto.json index 6c8615bd8..2ed5318ee 100644 --- a/openapi-contracts/src/test/resources/domains/voice/v2/svaml/menu/MenuItemDto.json +++ b/openapi-contracts/src/test/resources/domains/voice/v2/svaml/menu/MenuItemDto.json @@ -41,7 +41,7 @@ } ] }, - "onFail": [ + "onFailure": [ { "command": "hangup", "callName": "origin" From d62f72a62f130aee5693d4569548cf92284b9720 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Wed, 14 Oct 2026 16:54:00 +0200 Subject: [PATCH 41/50] Removed entries in CHANGELOG not rquired --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 586088fd2..15a8a5636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,8 +37,6 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[feature]** Voice V2 Svaml API: `describe` operation to get a human-readable description of the call flow of a SVAML payload - **[feature]** Voice V2 Svaml API: `validate` operation to check a SVAML payload against the expected schema and rules, in `NORMAL` or `STRICT` mode - **[feature]** Voice V2 Sinch Events: `validateAuthenticationHeader`, `parseEvent` and `serializeResponse` operations to handle the events sent by Sinch -- **[feature]** Voice V2 `ServiceResponse` exposes `serviceSecret`, returned by `services().create`, `services().get` and `services().update` -- **[design]** Voice V2 menu items: `onFail` is renamed to `onFailure` in `MenuItem` (`getOnFailure()` / `setOnFailure()`), following the API specification - **[doc]** New `voice/v2/calls/Start` snippet - **[doc]** New `voice/v2/calls/Get` snippet - **[doc]** New `voice/v2/calls/List` snippet From 15af2a4b22448505b3ed39a2ff4c86031eb4cd35 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 15 Oct 2026 08:29:21 +0200 Subject: [PATCH 42/50] docs: Recommend Voice API v2 for new projects --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 1db6001f6..4a227cf00 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,16 @@ You can find a complete example in [examples/sinch-events](https://github.com/si ### Voice API +**Note:** For all new projects, please use Voice API v2. This is especially important for projects involving: + +- AI capabilities like Streams and Voice Relay +- Voice campaigns like Batch calling + +You should only use the older Voice API v1 if one of the following is a strict requirement: + +- The project must be in General Availability (GA). +- The project requires In-App voice or Conferencing (until these features are available in v2). + The Voice API v1 uses application credentials. Set `applicationKey` and `applicationSecret` (both available on the [Apps dashboard](https://dashboard.sinch.com/voice/apps)); `voiceRegion` is optional and defaults to a global region: ```java From 2aca8c9c96f8b74f32a40440f652518f706b1170 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 15 Oct 2026 11:58:50 +0200 Subject: [PATCH 43/50] DEVEXP-1526: Pass the timeout to HttpClientApache in the Idempotency-Key test --- .../sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java index 22a480d54..0dae65b6a 100644 --- a/client/src/test/java/com/sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java +++ b/client/src/test/java/com/sinch/sdk/http/HttpClientApacheIdempotencyKeyTest.java @@ -11,6 +11,7 @@ import com.sinch.sdk.core.http.IdempotencyKey; import com.sinch.sdk.core.models.ServerConfiguration; import com.sinch.sdk.models.RetryConfiguration; +import com.sinch.sdk.models.TransportSettings; import java.io.IOException; import java.util.Collections; import java.util.Map; @@ -37,7 +38,8 @@ void setUp() throws IOException { client = new HttpClientApache( null, - new DefaultRetryManager(RetryConfiguration.builder().setMaxRetryCount(1).build())); + new DefaultRetryManager(RetryConfiguration.builder().setMaxRetryCount(1).build()), + TransportSettings.DEFAULT_TIMEOUT_SECONDS); } @AfterEach From 50a93a9fcdadf17ef714e25ccfb278e07d20a43f Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 15 Oct 2026 12:30:24 +0200 Subject: [PATCH 44/50] Bump Jackson to 2.21.7 to fix Dependabot vulnerabilities --- CHANGELOG.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5698ca781..7304b8a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. - **[test]** Extend `SinchEventsService.validateAuthenticationHeader` coverage ### Build -- **[dependency]** Bump `jackson.version` to `2.21.6` +- **[dependency]** Bump `jackson.version` to `2.21.7` to solve denial-of-service and unsafe deserialization vulnerabilities in `jackson-databind` (CVE-2026-19032, CVE-2026-68497, CVE-2026-83557, CVE-2026-91776 and CVE-2026-91777). ## v2.2.0 - 2026-09-17 diff --git a/pom.xml b/pom.xml index 49e440ecc..367a18a63 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ UTF-8 - 2.21.6 + 2.21.7 5.6.3 From 3c3a11f1dc9601dffad3dd80bb0a1588afb45ba0 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 15 Oct 2026 14:41:16 +0200 Subject: [PATCH 45/50] Bump central-publishing-maven-plugin to 0.12.0 to fix Central bundle validation --- CHANGELOG.md | 1 + pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7304b8a70..064399fdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ All notable changes to the **Sinch Java SDK** are documented in this file. ### Build - **[dependency]** Bump `jackson.version` to `2.21.7` to solve denial-of-service and unsafe deserialization vulnerabilities in `jackson-databind` (CVE-2026-19032, CVE-2026-68497, CVE-2026-83557, CVE-2026-91776 and CVE-2026-91777). +- **[releasing]** Bump `central-publishing-maven-plugin` to `0.12.0`: Maven Central rejected the release bundle built with Apache Maven 3.10.0 ## v2.2.0 - 2026-09-17 diff --git a/pom.xml b/pom.xml index 367a18a63..e2023d736 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 3.6.0 3.0.1 1.6 - 0.11.0 + 0.12.0 From 06f9a3f6ce46e0f94e9b9f6909c8ffdbf0df833c Mon Sep 17 00:00:00 2001 From: git Date: Thu, 15 Oct 2026 13:24:55 +0000 Subject: [PATCH 46/50] build (release): Bump version to 2.3.0 for sources --- client/src/main/com/sinch/sdk/SDK.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/com/sinch/sdk/SDK.java b/client/src/main/com/sinch/sdk/SDK.java index 68682caf9..ae311a749 100644 --- a/client/src/main/com/sinch/sdk/SDK.java +++ b/client/src/main/com/sinch/sdk/SDK.java @@ -3,6 +3,6 @@ public class SDK { public static final String NAME = "Sinch Java SDK"; - public static final String VERSION = "2.3.0-dev"; + public static final String VERSION = "2.3.0"; public static final String AUXILIARY_FLAG = ""; } From 32cce4148541fc75f9ab64beabbd4b80e88cda28 Mon Sep 17 00:00:00 2001 From: git Date: Thu, 15 Oct 2026 13:27:11 +0000 Subject: [PATCH 47/50] [release] Set release & tag: 2.3.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e2023d736..cc0efa2d3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.sinch.sdk sinch-sdk-java - 2.3.0-SNAPSHOT + 2.3.0 Sinch Java SDK @@ -37,7 +37,7 @@ https://github.com/sinch/sinch-sdk-java.git scm:git:${project.scm.url} scm:git:${project.scm.url} - HEAD + v2.3.0 From 213b23bcf582d7e81e403811aa8a9ce8b93add6f Mon Sep 17 00:00:00 2001 From: git Date: Thu, 15 Oct 2026 13:27:14 +0000 Subject: [PATCH 48/50] [release] Set next version: 2.4.0-SNAPSHOT --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index cc0efa2d3..87a92996e 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.sinch.sdk sinch-sdk-java - 2.3.0 + 2.4.0-SNAPSHOT Sinch Java SDK @@ -37,7 +37,7 @@ https://github.com/sinch/sinch-sdk-java.git scm:git:${project.scm.url} scm:git:${project.scm.url} - v2.3.0 + HEAD From 762bc89de9c0f6a1234187a35338839f3f96550c Mon Sep 17 00:00:00 2001 From: git Date: Thu, 15 Oct 2026 13:29:34 +0000 Subject: [PATCH 49/50] build (release): Set next version to 2.4.0-dev for sources --- client/src/main/com/sinch/sdk/SDK.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/com/sinch/sdk/SDK.java b/client/src/main/com/sinch/sdk/SDK.java index ae311a749..88ad4d526 100644 --- a/client/src/main/com/sinch/sdk/SDK.java +++ b/client/src/main/com/sinch/sdk/SDK.java @@ -3,6 +3,6 @@ public class SDK { public static final String NAME = "Sinch Java SDK"; - public static final String VERSION = "2.3.0"; + public static final String VERSION = "2.4.0-dev"; public static final String AUXILIARY_FLAG = ""; } From acc92d9e312ecd807a98440e6abf7f3f1464b6b7 Mon Sep 17 00:00:00 2001 From: Eduardo San Segundo Date: Thu, 15 Oct 2026 15:32:32 +0200 Subject: [PATCH 50/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 420434041..2397f8ffe 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Note: The `${sdk.version}` needs to be set according to the released version to | Messaging | [Conversation API](https://developers.sinch.com/docs/conversation/) | | | [SMS API](https://developers.sinch.com/docs/sms/) | | Voice and Video | [Voice API](https://developers.sinch.com/docs/voice/) | -| | [Voice API v2](https://developers.sinch.com/docs/voice-2.0) (preview) | +| | [Voice API v2](https://developers.sinch.com/docs/voice-2.0) | | Numbers | [Numbers API](https://developers.sinch.com/docs/numbers/) | | Verification | [Verification API](https://developers.sinch.com/docs/verification/) | | | [Number Lookup API](https://developers.sinch.com/docs/number-lookup-api-v2/) |