Skip to content

Commit

Permalink
Merge pull request #245 from dvonthenen/further-refactor-rest
Browse files Browse the repository at this point in the history
Implement TTS and Other REST Refactor
  • Loading branch information
davidvonthenen authored Mar 21, 2024
2 parents d4fd4e5 + c8c0606 commit 9a52ec9
Show file tree
Hide file tree
Showing 21 changed files with 767 additions and 116 deletions.
6 changes: 6 additions & 0 deletions Deepgram.Dev.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Streaming", "examples\strea
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prerecorded", "examples\prerecorded\Prerecorded.csproj", "{70B63CBA-1130-46D1-A022-6CD31C37C60E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Speak", "examples\speak\Speak.csproj", "{C3AA63DB-4555-4BEF-B2DD-89A3B19A265B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -33,6 +35,10 @@ Global
{70B63CBA-1130-46D1-A022-6CD31C37C60E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70B63CBA-1130-46D1-A022-6CD31C37C60E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70B63CBA-1130-46D1-A022-6CD31C37C60E}.Release|Any CPU.Build.0 = Release|Any CPU
{C3AA63DB-4555-4BEF-B2DD-89A3B19A265B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3AA63DB-4555-4BEF-B2DD-89A3B19A265B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3AA63DB-4555-4BEF-B2DD-89A3B19A265B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3AA63DB-4555-4BEF-B2DD-89A3B19A265B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
1 change: 0 additions & 1 deletion Deepgram.Tests/Fakes/MockHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ public static HttpClient CreateHttpClientWithResult<T>(
};
}


public static HttpClient CreateHttpClientWithException(Exception Exception)
{
return new HttpClient(new MockHttpMessageHandlerException(Exception))
Expand Down
2 changes: 1 addition & 1 deletion Deepgram.Tests/Fakes/MockHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT

namespace Deepgram.Tests.Fakes;

public class MockHttpMessageHandler<T>(T response, HttpStatusCode statusCode) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Expand All @@ -11,5 +12,4 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
StatusCode = statusCode,
Content = new StringContent(JsonSerializer.Serialize(response))
});

}
165 changes: 165 additions & 0 deletions Deepgram.Tests/UnitTests/ClientTests/SpeakClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2021-2024 Deepgram .NET SDK contributors. All Rights Reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT

using Deepgram.Models.Authenticate.v1;
using Deepgram.Models.Speak.v1;
using Deepgram.Clients.Speak.v1;
using NSubstitute;

namespace Deepgram.Tests.UnitTests.ClientTests;

public class SpeakClientTests
{
DeepgramClientOptions _options;
string _apiKey;

[SetUp]
public void Setup()
{
_options = new DeepgramClientOptions();
_apiKey = new Faker().Random.Guid().ToString();
}

//[Test]
//public async Task SpeakStream_Should_Call_PostFileAsync_Returning_SyncResponse()
//{
// //Arrange
// var expectedResponse = new AutoFaker<List<(Dictionary<string, string>, MemoryStream)>>().Generate();
// var speakSchema = new AutoFaker<SpeakSchema>().Generate();
// speakSchema.CallBack = null;
// var source = new TextSource("Hello World!");
// var keys = new List<string> { "key1", "key2" };
// var stringedOptions = QueryParameterUtil.GetParameters(speakSchema);
// var httpClient = MockHttpClient.CreateHttpClientWithResult(expectedResponse);

// var speakClient = Substitute.For<SpeakClient>(_apiKey, _options);


// speakClient.When(x => x.PostFileAsync<(Dictionary<string, string>, MemoryStream)>(Arg.Any<string>(), Arg.Any<HttpContent>(), Arg.Any<List<string>>())).DoNotCallBase();
// speakClient.PostFileAsync<List<(Dictionary<string, string>, MemoryStream)>>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<HttpContent>(), keys).Returns(expectedResponse);

// // Act
// var result = await speakClient.Stream(source, speakSchema);

// // Assert
// await speakClient.Received().PostFileAsync<(Dictionary<string, string>, MemoryStream)>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<HttpContent>(), keys);
// using (new AssertionScope())
// {
// result.Should().NotBeNull();
// // result.Should().BeAssignableTo<SyncResponse>();
// // result.Should().BeEquivalentTo(expectedResponse);
// }
//}

[Test]
public async Task StreamCallBack_With_CallBack_Property_Should_Call_PostAsync_Returning_SyncResponse()
{
//Arrange
var expectedResponse = new AutoFaker<AsyncResponse>().Generate();
var speakSchema = new AutoFaker<SpeakSchema>().Generate();
var source = new TextSource("Hello World!");
var stringedOptions = QueryParameterUtil.GetParameters(speakSchema);
var httpClient = MockHttpClient.CreateHttpClientWithResult(expectedResponse);

var speakClient = Substitute.For<SpeakClient>(_apiKey, _options);

speakClient.When(x => x.PostAsync<AsyncResponse>(Arg.Any<string>(), Arg.Any<HttpContent>())).DoNotCallBase();
speakClient.PostAsync<AsyncResponse>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<HttpContent>()).Returns(expectedResponse);

// Act
var result = await speakClient.StreamCallBack(source, null, speakSchema);

// Assert
await speakClient.Received().PostAsync<AsyncResponse>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<HttpContent>());
using (new AssertionScope())
{
result.Should().NotBeNull();
result.Should().BeAssignableTo<AsyncResponse>();
result.Should().BeEquivalentTo(expectedResponse);
}
}

[Test]
public async Task StreamCallBack_With_CallBack_Parameter_Should_Call_PostAsync_Returning_SyncResponse()
{
//Arrange
var expectedResponse = new AutoFaker<AsyncResponse>().Generate();
var speakSchema = new AutoFaker<SpeakSchema>().Generate();
// speakSchema is not null here as we first need to get the querystring with the callBack included
var stringedOptions = QueryParameterUtil.GetParameters(speakSchema);
var source = new TextSource("Hello World!");
var httpClient = MockHttpClient.CreateHttpClientWithResult(expectedResponse);

var speakClient = Substitute.For<SpeakClient>(_apiKey, _options);

speakClient.When(x => x.PostAsync<AsyncResponse>(Arg.Any<string>(), Arg.Any<HttpContent>())).DoNotCallBase();
speakClient.PostAsync<AsyncResponse>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<HttpContent>()).Returns(expectedResponse);

var callBack = speakSchema.CallBack;

//before we act to test this call with the callBack parameter and not the callBack property we need to null the callBack property
speakSchema.CallBack = null;

// Act
var result = await speakClient.StreamCallBack(source, callBack, speakSchema);

// Assert
await speakClient.Received().PostAsync<AsyncResponse>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<HttpContent>());
using (new AssertionScope())
{
result.Should().NotBeNull();
result.Should().BeAssignableTo<AsyncResponse>();
result.Should().BeEquivalentTo(expectedResponse);
}
}


[Test]
public async Task StreamCallBack_Throw_ArgumentException_With_CallBack_Property_And_CallBack_Parameter_Set()
{
//Arrange
var expectedResponse = new AutoFaker<AsyncResponse>().Generate();
var speakSchema = new AutoFaker<SpeakSchema>().Generate();
var stringedOptions = QueryParameterUtil.GetParameters(speakSchema);
var source = new TextSource("Hello World!");
var httpClient = MockHttpClient.CreateHttpClientWithResult(expectedResponse);

var speakClient = Substitute.For<SpeakClient>(_apiKey, _options);

speakClient.When(x => x.PostAsync<AsyncResponse>(Arg.Any<string>(), Arg.Any<HttpContent>())).DoNotCallBase();
speakClient.PostAsync<AsyncResponse>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<HttpContent>()).Returns(expectedResponse);
var callBack = speakSchema.CallBack;


// Act Assert
await speakClient.Invoking(y => y.StreamCallBack(source, callBack, speakSchema))
.Should().ThrowAsync<ArgumentException>();

await speakClient.DidNotReceive().PostAsync<AsyncResponse>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<StringContent>());
}

[Test]
public async Task StreamCallBack_Should_Throw_ArgumentException_With_No_CallBack_Set()
{
//Arrange
var expectedResponse = new AutoFaker<AsyncResponse>().Generate();
var speakSchema = new AutoFaker<SpeakSchema>().Generate();
var source = new TextSource("Hello World!");
// speakSchema is not null here as we first need to get the querystring with the callBack included
var stringedOptions = QueryParameterUtil.GetParameters(speakSchema);

var httpClient = MockHttpClient.CreateHttpClientWithResult(expectedResponse);
var speakClient = Substitute.For<SpeakClient>(_apiKey, _options);

speakClient.When(x => x.PostAsync<AsyncResponse>(Arg.Any<string>(), Arg.Any<HttpContent>())).DoNotCallBase();
speakClient.PostAsync<AsyncResponse>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<HttpContent>()).Returns(expectedResponse);
speakSchema.CallBack = null;

// Act Assert
await speakClient.Invoking(y => y.StreamCallBack(source, null, speakSchema))
.Should().ThrowAsync<ArgumentException>();

await speakClient.DidNotReceive().PostAsync<AsyncResponse>($"{UriSegments.SPEAK}?{stringedOptions}", Arg.Any<StringContent>());
}
}
100 changes: 79 additions & 21 deletions Deepgram/Abstractions/AbstractRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,17 @@ internal AbstractRestClient(string apiKey = "", DeepgramClientOptions? options =
_httpClient = HttpClientFactory.ConfigureDeepgram(_httpClient, apiKey, options);
}

// TODO: DYV is this needed? I am actually thinking not
// was in HttpClientWrapper class
//internal Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
//{
// return _httpClient.SendAsync(request, cancellationToken);
//}

/// <summary>
/// GET Rest Request
/// </summary>
/// <typeparam name="T">Type of class of response expected</typeparam>
/// <param name="uriSegment">request uri Endpoint</param>
/// <returns>Instance of T</returns>
public virtual async Task<T> GetAsync<T>(string uriSegment, CancellationToken cancellationToken = default)
public virtual async Task<T> GetAsync<T>(string uriSegment, CancellationToken cancellationToken = default, Dictionary<string, string>? addons = null)
{
try
{
var req = new HttpRequestMessage(HttpMethod.Get, uriSegment);
var req = new HttpRequestMessage(HttpMethod.Get, QueryParameterUtil.AppendQueryParameters(uriSegment, addons));

var response = await _httpClient.SendAsync(req, cancellationToken);
response.EnsureSuccessStatusCode();
Expand All @@ -68,11 +61,76 @@ public virtual async Task<T> GetAsync<T>(string uriSegment, CancellationToken ca
/// <param name="uriSegment">Uri for the api including the query parameters</param>
/// <param name="content">HttpContent as content for HttpRequestMessage</param>
/// <returns>Instance of T</returns>
public virtual async Task<T> PostAsync<T>(string uriSegment, HttpContent content, CancellationToken cancellationToken = default)
public virtual async Task<(Dictionary<string, string>, MemoryStream)> PostFileAsync<T>(string uriSegment, HttpContent content, List<string> keys, CancellationToken cancellationToken = default, Dictionary<string, string>? addons = null)
{
try
{
var req = new HttpRequestMessage(HttpMethod.Post, uriSegment) { Content = content };
var req = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.AppendQueryParameters(uriSegment, addons)) { Content = content };
var response = await _httpClient.SendAsync(req, cancellationToken);
response.EnsureSuccessStatusCode();

var result = new Dictionary<string, string>();

for (int i = 0; i < response.Headers.Count(); i++)
{
var key = response.Headers.ElementAt(i).Key.ToLower();
var value = response.Headers.GetValues(key).FirstOrDefault() ?? "";

var index = key.IndexOf("x-dg-");
if (index == 0)
{
var newKey = key.Substring(5);
if (keys.Contains(newKey))
{
result.Add(newKey, value);
continue;
}
}
index = key.IndexOf("dg-");
if (index == 0)
{
var newKey = key.Substring(3);
if (keys.Contains(newKey))
{
result.Add(newKey, value);
continue;
}
}
if (keys.Contains(key))
{
result.Add(key, value);
}
}

if (keys.Contains("content-type"))
{
result.Add("content-type", response.Content.Headers.ContentType?.MediaType ?? "");
}

MemoryStream stream = new MemoryStream();
await response.Content.CopyToAsync(stream);

return (result, stream);
}
catch (Exception ex)
{
Log.Exception(_logger, "POST", ex);
throw;
}
}

/// <summary>
/// Post method
/// </summary>
/// <typeparam name="T">Class type of what return type is expected</typeparam>
/// <param name="uriSegment">Uri for the api including the query parameters</param>
/// <param name="content">HttpContent as content for HttpRequestMessage</param>
/// <returns>Instance of T</returns>
public virtual async Task<T> PostAsync<T>(string uriSegment, HttpContent content, CancellationToken cancellationToken = default, Dictionary<string, string>? addons = null)
{
try
{
var req = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.AppendQueryParameters(uriSegment, addons)) { Content = content };
var response = await _httpClient.SendAsync(req, cancellationToken);
response.EnsureSuccessStatusCode();
var result = await RequestContentUtil.DeserializeAsync<T>(response);
Expand All @@ -92,11 +150,11 @@ public virtual async Task<T> PostAsync<T>(string uriSegment, HttpContent content
/// Delete Method for use with calls that do not expect a response
/// </summary>
/// <param name="uriSegment">Uri for the api including the query parameters</param>
public virtual async Task DeleteAsync(string uriSegment, CancellationToken cancellationToken = default)
public virtual async Task DeleteAsync(string uriSegment, CancellationToken cancellationToken = default, Dictionary<string, string>? addons = null)
{
try
{
var req = new HttpRequestMessage(HttpMethod.Delete, uriSegment);
var req = new HttpRequestMessage(HttpMethod.Delete, QueryParameterUtil.AppendQueryParameters(uriSegment, addons));
var response = await _httpClient.SendAsync(req, cancellationToken);
response.EnsureSuccessStatusCode();
}
Expand All @@ -113,11 +171,11 @@ public virtual async Task DeleteAsync(string uriSegment, CancellationToken cance
/// <typeparam name="T">Class Type of expected response</typeparam>
/// <param name="uriSegment">Uri for the api including the query parameters</param>
/// <returns>Instance of T or throws Exception</returns>
public virtual async Task<T> DeleteAsync<T>(string uriSegment, CancellationToken cancellationToken = default)
public virtual async Task<T> DeleteAsync<T>(string uriSegment, CancellationToken cancellationToken = default, Dictionary<string, string>? addons = null)
{
try
{
var req = new HttpRequestMessage(HttpMethod.Delete, uriSegment);
var req = new HttpRequestMessage(HttpMethod.Delete, QueryParameterUtil.AppendQueryParameters(uriSegment, addons));
var response = await _httpClient.SendAsync(req, cancellationToken);
response.EnsureSuccessStatusCode();
var result = await RequestContentUtil.DeserializeAsync<T>(response);
Expand All @@ -137,16 +195,16 @@ public virtual async Task<T> DeleteAsync<T>(string uriSegment, CancellationToken
/// <typeparam name="T">Class type of what return type is expected</typeparam>
/// <param name="uriSegment">Uri for the api including the query parameters</param>
/// <returns>Instance of T</returns>
public virtual async Task<T> PatchAsync<T>(string uriSegment, StringContent content, CancellationToken cancellationToken = default)
public virtual async Task<T> PatchAsync<T>(string uriSegment, StringContent content, CancellationToken cancellationToken = default, Dictionary<string, string>? addons = null)
{
try
{
#if NETSTANDARD2_0
var request = new HttpRequestMessage(new HttpMethod("PATCH"), uriSegment) { Content = content };
var request = new HttpRequestMessage(new HttpMethod("PATCH"), QueryParameterUtil.AppendQueryParameters(uriSegment, addons)) { Content = content };
var response = await _httpClient.SendAsync(request, cancellationToken);
#else
var req = new HttpRequestMessage(HttpMethod.Patch, uriSegment) { Content = content };
var response = await _httpClient.SendAsync(req, cancellationToken);
var request = new HttpRequestMessage(HttpMethod.Patch, QueryParameterUtil.AppendQueryParameters(uriSegment, addons)) { Content = content };
var response = await _httpClient.SendAsync(request, cancellationToken);

#endif
response.EnsureSuccessStatusCode();
Expand All @@ -168,11 +226,11 @@ public virtual async Task<T> PatchAsync<T>(string uriSegment, StringContent cont
/// <typeparam name="T">Class type of what return type is expected</typeparam>
/// <param name="uriSegment">Uri for the api</param>
/// <returns>Instance of T</returns>
public virtual async Task<T> PutAsync<T>(string uriSegment, StringContent content, CancellationToken cancellationToken = default)
public virtual async Task<T> PutAsync<T>(string uriSegment, StringContent content, CancellationToken cancellationToken = default, Dictionary<string, string>? addons = null)
{
try
{
var req = new HttpRequestMessage(HttpMethod.Put, uriSegment)
var req = new HttpRequestMessage(HttpMethod.Put, QueryParameterUtil.AppendQueryParameters(uriSegment, addons))
{
Content = content
};
Expand Down
Loading

0 comments on commit 9a52ec9

Please sign in to comment.