From 2400c351af6be60a0f53f9b63eac7560579d7480 Mon Sep 17 00:00:00 2001 From: Morgan Leroi Date: Mon, 29 Jan 2024 12:29:54 +0100 Subject: [PATCH] feat(csharp): add helpers (#2578) --- .../algoliasearch/Utils/ApiKeyEquals.cs | 51 ++ .../algoliasearch/Utils/ApiKeyOperation.cs | 21 + .../algoliasearch/Utils/Helpers.cs | 221 ++++++++ .../algoliasearch/Utils/ModelConverters.cs | 30 ++ .../csharp/Playground/Playgrounds/Search.cs | 184 ++++++- playground/csharp/Playground/Program.cs | 2 +- .../csharp/Playground/Utils/Configuration.cs | 2 +- .../Playground/Utils/PlaygroundHelper.cs | 18 + .../csharp/src/Algolia.Search.Tests.csproj | 1 + tests/output/csharp/src/tests/Helpers.cs | 471 ++++++++++++++++++ 10 files changed, 977 insertions(+), 24 deletions(-) create mode 100644 clients/algoliasearch-client-csharp/algoliasearch/Utils/ApiKeyEquals.cs create mode 100644 clients/algoliasearch-client-csharp/algoliasearch/Utils/ApiKeyOperation.cs create mode 100644 clients/algoliasearch-client-csharp/algoliasearch/Utils/Helpers.cs create mode 100644 clients/algoliasearch-client-csharp/algoliasearch/Utils/ModelConverters.cs create mode 100644 playground/csharp/Playground/Utils/PlaygroundHelper.cs create mode 100644 tests/output/csharp/src/tests/Helpers.cs diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Utils/ApiKeyEquals.cs b/clients/algoliasearch-client-csharp/algoliasearch/Utils/ApiKeyEquals.cs new file mode 100644 index 0000000000..d01aa51b5a --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Utils/ApiKeyEquals.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Algolia.Search.Models.Search +{ + public partial class ApiKey + { + /// + /// Compare two ApiKey objects + /// + /// + /// + public override bool Equals(object obj) + { + // We compare the properties of the object + // We DO NOT compare the null props of the obj. + if (obj is ApiKey other) + { + return + Description == other.Description && + QueryParameters == other.QueryParameters && + CheckSequence(Acl, other.Acl) && + CheckSequence(Indexes, other.Indexes) && + CheckSequence(Referers, other.Referers) && + CheckNullable(MaxHitsPerQuery, other.MaxHitsPerQuery) && + CheckNullable(MaxQueriesPerIPPerHour, other.MaxQueriesPerIPPerHour) && + CheckNullable(Validity, other.Validity); + } + + return base.Equals(obj); + } + + private bool CheckNullable(T objProps, T otherProps) + { + // if other is null, we don't compare the property + if (otherProps == null) + return true; + + return objProps != null && objProps.Equals(otherProps); + } + + private bool CheckSequence(List objProps, List otherProps) + { + // if other is null, we don't compare the property + if (otherProps == null) + return true; + + return objProps != null && objProps.SequenceEqual(otherProps); + } + } +} diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Utils/ApiKeyOperation.cs b/clients/algoliasearch-client-csharp/algoliasearch/Utils/ApiKeyOperation.cs new file mode 100644 index 0000000000..b1d9d2b222 --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Utils/ApiKeyOperation.cs @@ -0,0 +1,21 @@ +namespace Algolia.Search.Utils +{ + /// + /// ApiKey operations + /// + public enum ApiKeyOperation + { + /// + /// Add a new ApiKey + /// + Add, + /// + /// Delete an existing ApiKey + /// + Delete, + /// + /// Update an existing ApiKey + /// + Update, + } +} diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Utils/Helpers.cs b/clients/algoliasearch-client-csharp/algoliasearch/Utils/Helpers.cs new file mode 100644 index 0000000000..84e4b000bb --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Utils/Helpers.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Algolia.Search.Clients; +using Algolia.Search.Exceptions; +using Algolia.Search.Http; +using Algolia.Search.Models.Search; +using TaskStatus = Algolia.Search.Models.Search.TaskStatus; + +namespace Algolia.Search.Utils +{ + /// + /// A tool class to help with common tasks + /// + public static class Helpers + { + private const int DefaultMaxRetries = 50; + + /// + /// Wait for a task to complete with `indexName` and `taskID`. + /// + /// Algolia Search Client instance + /// The `indexName` where the operation was performed. + /// The `taskID` returned in the method response. + /// The maximum number of retry. 50 by default. (optional) + /// The requestOptions to send along with the query, they will be merged with the transporter requestOptions. (optional) + /// Cancellation token (optional) + public static async Task WaitForTaskAsync(this SearchClient client, string indexName, long taskId, + int maxRetries = DefaultMaxRetries, RequestOptions requestOptions = null, CancellationToken ct = default) + { + return await RetryUntil( + async () => await client.GetTaskAsync(indexName, taskId, requestOptions, ct), + resp => resp.Status == TaskStatus.Published, maxRetries, ct).ConfigureAwait(false); + } + + /// + /// Helper method that waits for an API key task to be processed. + /// + /// Algolia Search Client instance + /// The `operation` that was done on a `key`. + /// The key that has been added, deleted or updated. + /// Necessary to know if an `update` operation has been processed, compare fields of the response with it. (optional - mandatory if operation is UPDATE) + /// The maximum number of retry. 50 by default. (optional) + /// The requestOptions to send along with the query, they will be merged with the transporter requestOptions. (optional) + /// Cancellation token (optional) + public static async Task WaitForApiKeyAsync(this SearchClient client, + ApiKeyOperation operation, string key, + ApiKey apiKey = default, int maxRetries = DefaultMaxRetries, RequestOptions requestOptions = null, + CancellationToken ct = default) + { + if (operation == ApiKeyOperation.Update) + { + if (apiKey == null) + { + throw new AlgoliaException("`ApiKey` is required when waiting for an `update` operation."); + } + + return await RetryUntil(() => client.GetApiKeyAsync(key, requestOptions, ct), + resp => + { + var apiKeyResponse = new ApiKey + { + Acl = resp.Acl, + Description = resp.Description, + Indexes = resp.Indexes, + Referers = resp.Referers, + Validity = resp.Validity, + QueryParameters = resp.QueryParameters, + MaxHitsPerQuery = resp.MaxHitsPerQuery, + MaxQueriesPerIPPerHour = resp.MaxQueriesPerIPPerHour + }; + return apiKeyResponse.Equals(apiKey); + }, maxRetries: maxRetries, ct: ct).ConfigureAwait(false); + } + + var addedKey = new GetApiKeyResponse(); + + // check the status of the getApiKey method + await RetryUntil(async () => + { + try + { + addedKey = await client.GetApiKeyAsync(key, requestOptions, ct).ConfigureAwait(false); + // magic number to signify we found the key + return -2; + } + catch (AlgoliaApiException e) + { + return e.HttpErrorCode; + } + }, (status) => + { + return operation switch + { + ApiKeyOperation.Add => + // stop either when the key is created or when we don't receive 404 + status is -2 or not 404 and not 0, + ApiKeyOperation.Delete => + // stop when the key is not found + status == 404, + _ => false + }; + }, + maxRetries, ct + ); + return addedKey; + } + + /// + /// Iterate on the `browse` method of the client to allow aggregating objects of an index. + /// + /// + /// The index in which to perform the request. + /// The `browse` parameters. + /// The requestOptions to send along with the query, they will be forwarded to the `browse` method and merged with the transporter requestOptions. + /// The model of the record + public static async Task> BrowseObjectsAsync(this SearchClient client, string indexName, + BrowseParamsObject browseParams, + RequestOptions requestOptions = null) + { + browseParams.HitsPerPage = 1000; + var all = await CreateIterable>(async prevResp => + { + browseParams.Cursor = prevResp?.Cursor; + return await client.BrowseAsync(indexName, new BrowseParams(browseParams), requestOptions); + }, resp => resp is { Cursor: null }).ConfigureAwait(false); + + return all.SelectMany(u => u.Hits); + } + + /// + /// Iterate on the `SearchRules` method of the client to allow aggregating rules of an index. + /// + /// + /// The index in which to perform the request. + /// The `SearchRules` parameters + /// The requestOptions to send along with the query, they will be forwarded to the `searchRules` method and merged with the transporter requestOptions. + public static async Task> BrowseRulesAsync(this SearchClient client, string indexName, + SearchRulesParams searchRulesParams, + RequestOptions requestOptions = null) + { + const int hitsPerPage = 1000; + searchRulesParams.HitsPerPage = hitsPerPage; + + var all = await CreateIterable>(async (prevResp) => + { + var page = prevResp?.Item2 ?? 0; + var searchSynonymsResponse = await client.SearchRulesAsync(indexName, searchRulesParams, requestOptions); + return new Tuple(searchSynonymsResponse, page + 1); + }, resp => resp?.Item1 is { NbHits: < hitsPerPage }).ConfigureAwait(false); + + return all.SelectMany(u => u.Item1.Hits); + } + + + /// + /// Iterate on the `SearchSynonyms` method of the client to allow aggregating rules of an index. + /// + /// + /// The index in which to perform the request. + /// The `SearchSynonyms` parameters. + /// The requestOptions to send along with the query, they will be forwarded to the `searchSynonyms` method and merged with the transporter requestOptions. + public static async Task> BrowseSynonymsAsync(this SearchClient client, string indexName, + SearchSynonymsParams synonymsParams, + RequestOptions requestOptions = null) + { + const int hitsPerPage = 1000; + synonymsParams.HitsPerPage = hitsPerPage; + var all = await CreateIterable>(async (prevResp) => + { + synonymsParams.Page = prevResp?.Item2 ?? 0; + var searchSynonymsResponse = await client.SearchSynonymsAsync(indexName, synonymsParams, requestOptions); + return new Tuple(searchSynonymsResponse, (prevResp?.Item2 ?? 0) + 1); + }, resp => resp?.Item1 is { NbHits: < hitsPerPage }).ConfigureAwait(false); + + return all.SelectMany(u => u.Item1.Hits); + } + + private static async Task RetryUntil(Func> func, Func validate, + int maxRetries = DefaultMaxRetries, CancellationToken ct = default) + { + var retryCount = 0; + while (retryCount < maxRetries) + { + var resp = await func().ConfigureAwait(false); + if (validate(resp)) + { + return resp; + } + + await Task.Delay(NextDelay(retryCount), ct).ConfigureAwait(false); + retryCount++; + } + + throw new AlgoliaException( + "The maximum number of retries exceeded. (" + (retryCount + 1) + "/" + maxRetries + ")"); + } + + private static int NextDelay(int retryCount) + { + return Math.Min(retryCount * 200, 5000); + } + + private static async Task> CreateIterable(Func> executeQuery, + Func stopCondition) + { + var responses = new List(); + var current = default(TU); + do + { + var response = await executeQuery(current).ConfigureAwait(false); + current = response; + responses.Add(response); + } while (!stopCondition(current)); + + return responses; + } + } +} diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Utils/ModelConverters.cs b/clients/algoliasearch-client-csharp/algoliasearch/Utils/ModelConverters.cs new file mode 100644 index 0000000000..58fec372b9 --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Utils/ModelConverters.cs @@ -0,0 +1,30 @@ +using Algolia.Search.Models.Search; + +namespace Algolia.Search.Utils +{ + /// + /// A tool class to help model conversion + /// + public static class ModelConverters + { + /// + /// Convert a GetApiKeyResponse to an ApiKey + /// + /// + /// + public static ApiKey ToApiKey(this GetApiKeyResponse apiKey) + { + return new ApiKey + { + Acl = apiKey.Acl, + Description = apiKey.Description, + Indexes = apiKey.Indexes, + Referers = apiKey.Referers, + Validity = apiKey.Validity, + QueryParameters = apiKey.QueryParameters, + MaxHitsPerQuery = apiKey.MaxHitsPerQuery, + MaxQueriesPerIPPerHour = apiKey.MaxQueriesPerIPPerHour + }; + } + } +} diff --git a/playground/csharp/Playground/Playgrounds/Search.cs b/playground/csharp/Playground/Playgrounds/Search.cs index 08cbc9089e..068c78c3bd 100644 --- a/playground/csharp/Playground/Playgrounds/Search.cs +++ b/playground/csharp/Playground/Playgrounds/Search.cs @@ -1,6 +1,10 @@ +using System.Diagnostics; using Algolia.Search.Clients; +using Algolia.Search.Exceptions; using Algolia.Search.Models.Common; using Algolia.Search.Models.Search; +using Algolia.Search.Utils; +using Algolia.Utils; using Action = Algolia.Search.Models.Search.Action; namespace Algolia.Playgrounds; @@ -9,6 +13,8 @@ public static class SearchPlayground { public static async Task Run(Configuration configuration) { + const string defaultIndex = "test-csharp-new-client"; + Console.WriteLine("------------------------------------"); Console.WriteLine("Starting Search API playground"); Console.WriteLine("------------------------------------"); @@ -25,75 +31,93 @@ public static async Task Run(Configuration configuration) // Save a single object Console.WriteLine("--- Save a single object `SaveObjectAsync` ---"); - var saved = await client.SaveObjectAsync("test-csharp-new-client", + var saved = await client.SaveObjectAsync(defaultIndex, new { ObjectID = "test2", value = "test", otherValue = "otherValue" }); - Console.WriteLine(saved.ObjectID); + + await PlaygroundHelper.Start($"Saving record ObjectID=`{saved.ObjectID}` - Async TaskID: `{saved.TaskID}`", + () => client.WaitForTaskAsync(defaultIndex, saved.TaskID), $"Record ObjectID=`{saved.ObjectID}` saved !"); // Set settings on index Console.WriteLine("--- Set setting on index `SetSettingsAsync` ---"); - var updatedAtResponse = await client.SetSettingsAsync("test-csharp-new-client", new IndexSettings() + var updatedAtResponse = await client.SetSettingsAsync(defaultIndex, new IndexSettings() { AttributesForFaceting = new List { "searchable(value)", "searchable(otherValue)" }, SearchableAttributes = new List { "value", "otherValue" } }); - Console.WriteLine(updatedAtResponse.TaskID); + + await PlaygroundHelper.Start( + $"Saving new settings on index `{defaultIndex}` - Async TaskID: `{updatedAtResponse.TaskID}`", + () => client.WaitForTaskAsync(defaultIndex, updatedAtResponse.TaskID), "New settings applied !"); // Save multiple objects Console.WriteLine("--- Save a multiple objects `BatchAsync` ---"); - var requests = new List() + var requests = new List { new(Action.AddObject, new { ObjectID = "test3", value = "batch1", otherValue = "otherValue1" }), new(Action.AddObject, new { ObjectID = "test4", value = "batch2", otherValue = "otherValue2" }), new(Action.AddObject, new { ObjectID = "test5", value = "batch3", otherValue = "otherValue3" }), }; - var batch = await client.BatchAsync("test-csharp-new-client", new BatchWriteParams(requests)); - batch.ObjectIDs.ForEach(Console.WriteLine); + var batch = await client.BatchAsync(defaultIndex, new BatchWriteParams(requests)); + + await PlaygroundHelper.Start( + $"Saving new records - Async TaskID: `{batch.TaskID}`", + () => client.WaitForTaskAsync(defaultIndex, updatedAtResponse.TaskID), "Records saved !"); // Browse all objects - Console.WriteLine("--- Browse all objects `BrowseAsync` ---"); - var r = await client.BrowseAsync("test-csharp-new-client"); - r.Hits.ForEach(h => Console.WriteLine(h.ObjectID)); + Console.WriteLine("--- Browse all objects, one page `BrowseAsync` ---"); + var r = await client.BrowseAsync(defaultIndex, + new BrowseParams(new BrowseParamsObject { HitsPerPage = 100 })); + r.Hits.ForEach(h => Console.WriteLine($" - Record ObjectID: {h.ObjectID}")); + + // Browse Helper, to fetch all pages + Console.WriteLine("--- Browse all objects, all pages `BrowseObjectsAsync` ---"); + var results = await client.BrowseObjectsAsync(defaultIndex, new BrowseParamsObject + { + HitsPerPage = 1 + }); + + results.ToList().ForEach(h => Console.WriteLine($" - Record ObjectID: {h.ObjectID}")); // Get Objects - Console.WriteLine("--- Get Objects `GetObjectsAsync` ---"); + Console.WriteLine("--- Get Objects, with specific attributes `GetObjectsAsync` ---"); var getObjRequests = new List { - new("test2", "test-csharp-new-client") + new("test2", defaultIndex) { AttributesToRetrieve = new List { "otherValue" } }, - new("test3", "test-csharp-new-client") + new("test3", defaultIndex) { AttributesToRetrieve = new List { "otherValue" } }, }; var getObjResults = await client.GetObjectsAsync(new GetObjectsParams(getObjRequests)); - getObjResults.Results.ForEach(testObject => Console.WriteLine(testObject.otherValue)); + getObjResults.Results.ForEach(t => Console.WriteLine($" - Record ObjectID: {t.ObjectID} - Property `otherValue`: {t.otherValue}")); // Search single index Console.WriteLine("--- Search single index `SearchSingleIndexAsync` ---"); - var t = await client.SearchSingleIndexAsync("test-csharp-new-client"); - t.Hits.ForEach(h => Console.WriteLine(h.ObjectID)); + var t = await client.SearchSingleIndexAsync(defaultIndex); + t.Hits.ForEach(h => Console.WriteLine($" - Record ObjectID: {h.ObjectID}")); // Search Console.WriteLine("--- Search multiple indices `SearchAsync` ---"); var searchQueries = new List { - new(new SearchForHits("test-csharp-new-client")), - new(new SearchForHits("test-csharp-new-client")), - new(new SearchForFacets("otherValue", "test-csharp-new-client", SearchTypeFacet.Facet)), + new(new SearchForHits(defaultIndex)), + new(new SearchForHits(defaultIndex)), + new(new SearchForFacets("otherValue", defaultIndex, SearchTypeFacet.Facet)), }; var search = await client.SearchAsync(new SearchMethodParams(searchQueries)); search.Results.ForEach(result => { if (result.IsSearchResponse()) { - Console.WriteLine("Hits: " + result.AsSearchResponse().Hits.First().ObjectID); + Console.WriteLine($"Record with Hits: ObjectID = {result.AsSearchResponse().Hits.First().ObjectID}"); } else if (result.IsSearchForFacetValuesResponse()) { - Console.WriteLine("Facet: " + result.AsSearchForFacetValuesResponse().FacetHits.First().Value); + Console.WriteLine("Record with Facet. Facet value = " + result.AsSearchForFacetValuesResponse().FacetHits.First().Value); } else { @@ -106,7 +130,123 @@ public static async Task Run(Configuration configuration) var tMetis = await metisClient.SearchSingleIndexAsync("008_jobs_v2_nosplit__contents__default"); foreach (var tMetisAdditionalProperty in tMetis.AdditionalProperties) { - Console.WriteLine(tMetisAdditionalProperty.Key + " : " + tMetisAdditionalProperty.Value); + Console.WriteLine($" - Additional property found {tMetisAdditionalProperty.Key} : {tMetisAdditionalProperty.Value}"); + } + + // API Key + Console.WriteLine("--- Add new api key `AddApiKeyAsync` ---"); + var addApiKeyResponse = await client.AddApiKeyAsync(new ApiKey() + { + Acl = new List { Acl.Browse, Acl.Search }, Description = "A test key", + Indexes = new List { defaultIndex } + }); + var createdApiKey = await PlaygroundHelper.Start($"Saving new API Key",async () => + await client.WaitForApiKeyAsync(ApiKeyOperation.Add, addApiKeyResponse.Key), "New key has been created !"); + + Console.WriteLine("--- Update api key `UpdateApiKeyAsync` ---"); + var modifiedApiKey = createdApiKey.ToApiKey(); + modifiedApiKey.Description = "Updated description"; + + var updateApiKey = await client.UpdateApiKeyAsync(addApiKeyResponse.Key, modifiedApiKey); + await PlaygroundHelper.Start("Updating API Key`",async () => + await client.WaitForApiKeyAsync(ApiKeyOperation.Update, updateApiKey.Key, modifiedApiKey), "Key updated !"); + + Console.WriteLine("--- Delete api key `UpdateApiKeyAsync` ---"); + await client.DeleteApiKeyAsync(addApiKeyResponse.Key); + await PlaygroundHelper.Start("Deleting API Key",async () => + await client.WaitForApiKeyAsync(ApiKeyOperation.Delete, updateApiKey.Key), "Key deleted !"); + + // Add Synonyms + Console.WriteLine("--- Add Synonyms `SaveSynonymsAsync` ---"); + var synonymsResponse = await client.SaveSynonymsAsync(defaultIndex, + new List + { + new() + { + Type = SynonymType.Onewaysynonym, ObjectID = "tshirt", + Synonyms = new List { "tshirt", "shirt", "slipover" }, Input = "tshirt" + }, + new() + { + Type = SynonymType.Onewaysynonym, ObjectID = "trousers", + Synonyms = new List { "trousers", "jeans", "pantaloons" }, Input = "trousers" + }, + new() + { + Type = SynonymType.Onewaysynonym, ObjectID = "shoes", + Synonyms = new List { "shoes", "boots", "sandals" }, Input = "shoes" + }, + }).ConfigureAwait(false); + + await PlaygroundHelper.Start("Creating new Synonyms - Async TaskID: `{synonymsResponse.TaskID}`",async () => + await client.WaitForTaskAsync(defaultIndex, synonymsResponse.TaskID), "New Synonyms has been created !"); + + // Search Synonyms + Console.WriteLine("--- Search Synonyms `SearchSynonymsAsync` ---"); + var searchSynonymsAsync = await client + .SearchSynonymsAsync(defaultIndex, new SearchSynonymsParams { Query = "", Type = SynonymType.Onewaysynonym, HitsPerPage = 1}) + .ConfigureAwait(false); + Console.WriteLine(searchSynonymsAsync.Hits.Count); + + // Browse Synonyms + // var configuredTaskAwaitable = await client + // .BrowseSynonymsAsync("test-csharp-new-client", SynonymType.Onewaysynonym, new SearchSynonymsParams { Query = "" }) + // .ConfigureAwait(false); + // configuredTaskAwaitable.ToList().ForEach(s => Console.WriteLine("Found :" + string.Join(',', s.Synonyms))); + + // Add Rule + Console.WriteLine("--- Create new Rule `SaveRulesAsync` ---"); + var saveRulesAsync = await client.SaveRulesAsync(defaultIndex, + new List + { + new() + { + ObjectID = "TestRule1", Description = "Test", + Consequence = + new Consequence { Promote = new List { new(new PromoteObjectID("test3", 1)) } }, + Conditions = new List + { new() { Anchoring = Anchoring.Contains, Context = "shoes", Pattern = "test" } } + }, + new() + { + ObjectID = "TestRule2", Description = "Test", + Consequence = + new Consequence { Promote = new List { new(new PromoteObjectID("test4", 1)) } }, + Conditions = new List + { new() { Anchoring = Anchoring.Contains, Context = "shoes", Pattern = "test" } } + }, + new() + { + ObjectID = "TestRule3", Description = "Test", + Consequence = + new Consequence { Promote = new List { new(new PromoteObjectID("test5", 1)) } }, + Conditions = new List + { new() { Anchoring = Anchoring.Contains, Context = "shoes", Pattern = "test" } } + } + }).ConfigureAwait(false); + + await PlaygroundHelper.Start($"Saving new Rule - Async TaskID: `{saveRulesAsync.TaskID}`", async () => await client.WaitForTaskAsync(defaultIndex, saveRulesAsync.TaskID), "New Rule has been created !"); + + Console.WriteLine("--- Error Handling ---"); + try + { + await client.SaveRulesAsync(defaultIndex, + new List + { + new() + { + ObjectID = "TestRule1", Description = "Test", + Consequence = + new Consequence { Promote = new List { new(new PromoteObjectID("test3", 1)) } }, + Conditions = new List + // Error, no Context set + { new() { Anchoring = Anchoring.Contains, Context = "shoes" } } + } + }).ConfigureAwait(false); + } + catch (AlgoliaApiException e) + { + Console.WriteLine($"Message: {e.Message} - Status {e.HttpErrorCode}"); } } } diff --git a/playground/csharp/Playground/Program.cs b/playground/csharp/Playground/Program.cs index 0e20e279b6..36d65b01bf 100644 --- a/playground/csharp/Playground/Program.cs +++ b/playground/csharp/Playground/Program.cs @@ -44,6 +44,7 @@ await Ingestion.Run(config); break; case "all": + await SearchPlayground.Run(config); await ABTesting.Run(config); await Analytics.Run(config); await Insights.Run(config); @@ -51,7 +52,6 @@ await Personalization.Run(config); await QuerySuggestions.Run(config); await Recommend.Run(config); - await SearchPlayground.Run(config); await Ingestion.Run(config); break; } diff --git a/playground/csharp/Playground/Utils/Configuration.cs b/playground/csharp/Playground/Utils/Configuration.cs index 06bd60c3fb..877757805b 100644 --- a/playground/csharp/Playground/Utils/Configuration.cs +++ b/playground/csharp/Playground/Utils/Configuration.cs @@ -6,7 +6,7 @@ public static class Config { public static Configuration Load() { - DotEnv.Load(options: new DotEnvOptions(ignoreExceptions: false, envFilePaths: new[] { "../.env", "./Playground/.env"})); + DotEnv.Load(options: new DotEnvOptions(ignoreExceptions: false, probeForEnv:true, envFilePaths: new[] { "../.env", "./Playground/.env"})); return new Configuration { AppId = GetEnvVariable("ALGOLIA_APPLICATION_ID"), diff --git a/playground/csharp/Playground/Utils/PlaygroundHelper.cs b/playground/csharp/Playground/Utils/PlaygroundHelper.cs new file mode 100644 index 0000000000..cfc5e2286c --- /dev/null +++ b/playground/csharp/Playground/Utils/PlaygroundHelper.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; + +namespace Algolia.Utils; + +public class PlaygroundHelper +{ + public static async Task Start(string startMessage, Func> ac, string endMessage) + { + Console.WriteLine(startMessage); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var waitResult = await ac().ConfigureAwait(false); + stopwatch.Stop(); + Console.WriteLine($" Task took {stopwatch.ElapsedMilliseconds}ms"); + Console.WriteLine(endMessage); + return waitResult; + } +} diff --git a/tests/output/csharp/src/Algolia.Search.Tests.csproj b/tests/output/csharp/src/Algolia.Search.Tests.csproj index 9583ecaabf..13ee986413 100644 --- a/tests/output/csharp/src/Algolia.Search.Tests.csproj +++ b/tests/output/csharp/src/Algolia.Search.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/tests/output/csharp/src/tests/Helpers.cs b/tests/output/csharp/src/tests/Helpers.cs new file mode 100644 index 0000000000..0c91dd9eda --- /dev/null +++ b/tests/output/csharp/src/tests/Helpers.cs @@ -0,0 +1,471 @@ +using System.Text; +using Algolia.Search.Clients; +using Algolia.Search.Exceptions; +using Algolia.Search.Http; +using Algolia.Search.Models.Search; +using Algolia.Search.Utils; +using Moq; +using Newtonsoft.Json; +using Xunit; +using TaskStatus = Algolia.Search.Models.Search.TaskStatus; + +namespace Algolia.Search.Tests.tests; + +public class HelpersTests +{ + [Fact] + public async Task ShouldWaitForTask() + { + var httpMock = new Mock(); + var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object); + + httpMock + .SetupSequence(c => + c.SendRequestAsync( + It.Is(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/test/task/12345")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + // First call return a task that is not published + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new GetTaskResponse { Status = TaskStatus.NotPublished } + ) + ) + ) + } + ) + ) + // Second call return a task that is published + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject(new GetTaskResponse { Status = TaskStatus.Published }) + ) + ) + } + ) + ); + + // Wait for the task to be published + await client.WaitForTaskAsync("test", 12345); + + // Verify that the request has been called twice + httpMock.Verify( + m => + m.SendRequestAsync( + It.Is(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/test/task/12345")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(2) + ); + } + + [Fact] + public async Task ShouldWaitForApiKey() + { + var httpMock = new Mock(); + var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object); + + httpMock + .SetupSequence(c => + c.SendRequestAsync( + It.Is(r => r.Uri.AbsolutePath.EndsWith("/1/keys/my-key")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + // First call throw an exception + .Throws(new AlgoliaApiException("Oupss", 0)) + // Next call return a 404 + .Returns(Task.FromResult(new AlgoliaHttpResponse { HttpStatusCode = 404 })) + // Third call return a Http 200 + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new GetApiKeyResponse() { CreatedAt = 12, Acl = new List() } + ) + ) + ) + } + ) + ); + + await client.WaitForApiKeyAsync(ApiKeyOperation.Add, "my-key"); + + // Verify that the request has been called three times + httpMock.Verify( + m => + m.SendRequestAsync( + It.Is(r => r.Uri.AbsolutePath.EndsWith("/1/keys/my-key")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(3) + ); + } + + [Fact] + public async Task ShouldBrowseObjects() + { + var httpMock = new Mock(); + var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object); + + httpMock + .SetupSequence(c => + c.SendRequestAsync( + It.Is(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/my-test-index/browse")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + // First call return two Hits with a cursor + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new BrowseResponse() + { + Hits = new List { new(), new() }, + Cursor = "A cursor ...", + HitsPerPage = 1, + NbHits = 2, + NbPages = 3, + Page = 0, + ProcessingTimeMS = 1, + Query = "", + VarParams = "" + } + ) + ) + ) + } + ) + ) + // Second call return two Hits with a cursor + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new BrowseResponse() + { + Hits = new List { new(), new() }, + Cursor = "Another cursor ...", + HitsPerPage = 1, + NbHits = 2, + NbPages = 3, + Page = 0, + ProcessingTimeMS = 1, + Query = "", + VarParams = "" + } + ) + ) + ) + } + ) + ) + // Third call return one hit with no cursor + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new BrowseResponse() + { + Hits = new List { new() }, + Cursor = null, + HitsPerPage = 1, + NbHits = 2, + NbPages = 3, + Page = 0, + ProcessingTimeMS = 1, + Query = "", + VarParams = "" + } + ) + ) + ) + } + ) + ); + + var browseObjectsAsync = await client.BrowseObjectsAsync( + "my-test-index", + new BrowseParamsObject() + ); + + // Verify that the request has been called three times + httpMock.Verify( + m => + m.SendRequestAsync( + It.Is(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/my-test-index/browse")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(3) + ); + + Assert.Equal(5, browseObjectsAsync.Count()); + } + + [Fact] + public async Task ShouldBrowseSynonyms() + { + var httpMock = new Mock(); + var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object); + + httpMock + .SetupSequence(c => + c.SendRequestAsync( + It.Is(r => + r.Uri.AbsolutePath.EndsWith("/1/indexes/my-test-index/synonyms/search") + ), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + // First call return 1000 Hits + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new SearchSynonymsResponse() + { + Hits = new List() + { + new() { ObjectID = "XXX", Type = SynonymType.Altcorrection1 }, + new() { ObjectID = "XXX", Type = SynonymType.Altcorrection1 } + }, // Not 1000 but it doesn't matter + NbHits = 1000, + } + ) + ) + ) + } + ) + ) + // Second call return again 1000 Hits + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new SearchSynonymsResponse() + { + Hits = new List() + { + new() { ObjectID = "XXX", Type = SynonymType.Altcorrection1 }, + new() { ObjectID = "XXX", Type = SynonymType.Altcorrection1 } + }, // Not 1000 but it doesn't matter + NbHits = 1000, + } + ) + ) + ) + } + ) + ) + // Third call return 999 Hits + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new SearchSynonymsResponse + { + Hits = new List + { + new() { ObjectID = "XXX", Type = SynonymType.Altcorrection1 }, + new() { ObjectID = "XXX", Type = SynonymType.Altcorrection1 } + }, // Not 1000 but it doesn't matter + NbHits = 999, + } + ) + ) + ) + } + ) + ); + + var browseSynonymsAsync = await client.BrowseSynonymsAsync( + "my-test-index", + new SearchSynonymsParams() + ); + + // Verify that the request has been called three times + httpMock.Verify( + m => + m.SendRequestAsync( + It.Is(r => + r.Uri.AbsolutePath.EndsWith("/1/indexes/my-test-index/synonyms/search") + ), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(3) + ); + + Assert.Equal(6, browseSynonymsAsync.Count()); + } + + [Fact] + public async Task ShouldBrowseRules() + { + var httpMock = new Mock(); + var client = new SearchClient(new SearchConfig("test-app-id", "test-api-key"), httpMock.Object); + + httpMock + .SetupSequence(c => + c.SendRequestAsync( + It.Is(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/my-test-index/rules/search")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + // First call return 1000 Hits + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new SearchRulesResponse + { + Page = 0, + NbPages = 2, + Hits = new List + { + new() { ObjectID = "XXX" }, + new() { ObjectID = "XXX" } + }, // Not 1000 but it doesn't matter + NbHits = 1000, + } + ) + ) + ) + } + ) + ) + // Second call return again 1000 Hits + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new SearchRulesResponse + { + Page = 0, + NbPages = 2, + Hits = new List + { + new() { ObjectID = "XXX" }, + new() { ObjectID = "XXX" } + }, // Not 1000 but it doesn't matter + NbHits = 1000, + } + ) + ) + ) + } + ) + ) + // Third call return 999 Hits + .Returns( + Task.FromResult( + new AlgoliaHttpResponse + { + HttpStatusCode = 200, + Body = new MemoryStream( + Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + new SearchRulesResponse + { + Page = 0, + NbPages = 2, + Hits = new List + { + new() { ObjectID = "XXX" }, + new() { ObjectID = "XXX" } + }, // Not 1000 but it doesn't matter + NbHits = 999, + } + ) + ) + ) + } + ) + ); + + var browseSynonymsAsync = await client.BrowseRulesAsync( + "my-test-index", + new SearchRulesParams() + ); + + // Verify that the request has been called three times + httpMock.Verify( + m => + m.SendRequestAsync( + It.Is(r => r.Uri.AbsolutePath.EndsWith("/1/indexes/my-test-index/rules/search")), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), + Times.Exactly(3) + ); + + Assert.Equal(6, browseSynonymsAsync.Count()); + } +}