diff --git a/src/Meilisearch/DefaultFormattable.cs b/src/Meilisearch/DefaultFormattable.cs new file mode 100644 index 00000000..b31fbd6f --- /dev/null +++ b/src/Meilisearch/DefaultFormattable.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace Meilisearch +{ + /// + /// The default implementation of + /// + /// + /// + public class DefaultFormattable : IFormatContainer + { + /// + /// Creates a formatted document + /// + /// + /// + public DefaultFormattable(TOriginal original, TFormatted formatted) + { + Original = original; + Formatted = formatted; + } + + /// + /// The original document + /// + public TOriginal Original { get; } + + /// + /// The formatted document + /// + [JsonPropertyName("_formatted")] + public TFormatted Formatted { get; } + } +} diff --git a/src/Meilisearch/IFormatContainer.cs b/src/Meilisearch/IFormatContainer.cs new file mode 100644 index 00000000..52c1b503 --- /dev/null +++ b/src/Meilisearch/IFormatContainer.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace Meilisearch +{ + /// + /// Used to receive document formatting information + /// + /// Original data type + /// Formatted data type + [JsonConverter(typeof(IFormatContainerJsonConverterFactory))] + public interface IFormatContainer + { + /// + /// The original result + /// + TOriginal Original { get; } + + /// + /// The formatted result + /// + TFormatted Formatted { get; } + } +} diff --git a/src/Meilisearch/IFormatContainerJsonConverter.cs b/src/Meilisearch/IFormatContainerJsonConverter.cs new file mode 100644 index 00000000..55196706 --- /dev/null +++ b/src/Meilisearch/IFormatContainerJsonConverter.cs @@ -0,0 +1,64 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Meilisearch +{ + public class IFormatContainerJsonConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsInterface + && typeToConvert.IsGenericType + && (typeToConvert.GetGenericTypeDefinition() == typeof(IFormatContainer<,>)); + } + + public override JsonConverter CreateConverter( + Type typeToConvert, + JsonSerializerOptions options + ) + { + var genericArgs = typeToConvert.GetGenericArguments(); + var converterType = typeof(IFormatContainerJsonConverter<,>).MakeGenericType( + genericArgs[0], + genericArgs[1] + ); + var converter = (JsonConverter)Activator.CreateInstance(converterType); + return converter; + } + } + + public class IFormatContainerJsonConverter + : JsonConverter> + where TFormatted : class + where TOriginal : class + { + public override IFormatContainer Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options + ) + { + var document = JsonSerializer.Deserialize(ref reader, options); + var original = document.Deserialize(options); + + if (document.TryGetProperty("_formatted", out var formattedElement)) + { + var formatted = formattedElement.Deserialize(options); + return new DefaultFormattable(original, formatted); + } + return new DefaultFormattable(original, null); + } + + public override void Write( + Utf8JsonWriter writer, + IFormatContainer value, + JsonSerializerOptions options + ) + { + var serialized = JsonSerializer.SerializeToNode(value.Original, options).AsObject(); + serialized["_formatted"] = JsonSerializer.SerializeToNode(value.Formatted, options); + serialized.WriteTo(writer, options); + } + } +} diff --git a/src/Meilisearch/Index.Documents.cs b/src/Meilisearch/Index.Documents.cs index 88f35761..06167aab 100644 --- a/src/Meilisearch/Index.Documents.cs +++ b/src/Meilisearch/Index.Documents.cs @@ -456,16 +456,8 @@ public async Task DeleteAllDocumentsAsync(CancellationToken cancellati .ConfigureAwait(false); } - /// - /// Search documents according to search parameters. - /// - /// Query Parameter with Search. - /// Attributes to search. - /// The cancellation token for this call. - /// Type parameter to return. - /// Returns Enumerable of items. - public async Task> SearchAsync(string query, - SearchQuery searchAttributes = default(SearchQuery), CancellationToken cancellationToken = default) + private async Task<(SearchQuery, HttpResponseMessage)> SendSearchRequest(string query, + SearchQuery searchAttributes = default, CancellationToken cancellationToken = default) { SearchQuery body; if (searchAttributes == null) @@ -483,9 +475,54 @@ public async Task DeleteAllDocumentsAsync(CancellationToken cancellati Constants.JsonSerializerOptionsRemoveNulls, cancellationToken: cancellationToken) .ConfigureAwait(false); - return await responseMessage.Content - .ReadFromJsonAsync>(cancellationToken: cancellationToken) + return (body, responseMessage); + } + + /// + /// Search documents according to search parameters. + /// + /// Query Parameter with Search. + /// Attributes to search. + /// The cancellation token for this call. + /// Type parameter to return. + /// Returns Enumerable of items. + public async Task> SearchAsync(string query, + SearchQuery searchAttributes = default, CancellationToken cancellationToken = default) + { + + var (body, responseMessage) = await SendSearchRequest(query, searchAttributes, cancellationToken); + + return body.Page != null || body.HitsPerPage != null + ? await responseMessage.Content + .ReadFromJsonAsync>(cancellationToken: cancellationToken) + .ConfigureAwait(false) + : (ISearchable)await responseMessage.Content + .ReadFromJsonAsync>(cancellationToken: cancellationToken) .ConfigureAwait(false); } + + /// + /// Search documents according to search parameters, Including format. + /// + /// Query Parameter with Search. + /// Attributes to search. + /// The cancellation token for this call. + /// Type parameter to return. + /// formatted document type. + /// Returns Enumerable of items. + public async Task>> SearchAsync(string query, + SearchQuery searchAttributes = default, CancellationToken cancellationToken = default) + { + var (body, responseMessage) = await SendSearchRequest(query, searchAttributes, cancellationToken); + + return body.Page != null || body.HitsPerPage != null + ? await responseMessage.Content + .ReadFromJsonAsync>>(cancellationToken: cancellationToken) + .ConfigureAwait(false) + : (ISearchable>)await responseMessage.Content + .ReadFromJsonAsync>>(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + } } diff --git a/tests/Meilisearch.Tests/Movie.cs b/tests/Meilisearch.Tests/Movie.cs index fe279502..5a92c7e7 100644 --- a/tests/Meilisearch.Tests/Movie.cs +++ b/tests/Meilisearch.Tests/Movie.cs @@ -34,16 +34,4 @@ public class MovieWithIntId public string Genre { get; set; } } - - public class FormattedMovie - { - public string Id { get; set; } - - public string Name { get; set; } - - public string Genre { get; set; } - -#pragma warning disable SA1300 - public Movie _Formatted { get; set; } - } } diff --git a/tests/Meilisearch.Tests/SearchTests.cs b/tests/Meilisearch.Tests/SearchTests.cs index 19633a93..68539e9d 100644 --- a/tests/Meilisearch.Tests/SearchTests.cs +++ b/tests/Meilisearch.Tests/SearchTests.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; @@ -32,6 +33,33 @@ public async Task InitializeAsync() public Task DisposeAsync() => Task.CompletedTask; + [Fact] + public async Task TestJsonConverter() + { + MovieWithIntId[] movies = + { + new MovieWithIntId { Id = 1, Name = "Batman" }, + new MovieWithIntId { Id = 2, Name = "Reservoir Dogs" }, + new MovieWithIntId { Id = 3, Name = "Taxi Driver" }, + new MovieWithIntId { Id = 4, Name = "Interstellar" }, + new MovieWithIntId { Id = 5, Name = "Titanic" }, + }; ; + var formattable = movies + .Select(x => new DefaultFormattable(x, new Movie() + { + Id = x.Id.ToString(), + Genre = x.Genre, + Name = x.Name + })) + .Cast>(); + + var elements = formattable.Select(x => JsonSerializer.SerializeToElement(x)).ToList(); + elements.Should().AllSatisfy(x => x.GetProperty("_formatted").Should().NotBeNull()); + + var deserialized = elements.Select(x => JsonSerializer.Deserialize>(x)).ToList(); + deserialized.Should().AllSatisfy(x => x.Formatted.Should().NotBeNull()); + } + [Fact] public async Task BasicSearch() { @@ -107,46 +135,46 @@ public async Task CustomSearchWithAttributesToHighlight() task.TaskUid.Should().BeGreaterOrEqualTo(0); await _basicIndex.WaitForTaskAsync(task.TaskUid); - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { AttributesToHighlight = new string[] { "name" } }); movies.Hits.Should().NotBeEmpty(); - movies.Hits.First().Id.Should().NotBeEmpty(); - movies.Hits.First().Name.Should().NotBeEmpty(); - movies.Hits.First().Genre.Should().NotBeEmpty(); - movies.Hits.First()._Formatted.Name.Should().NotBeEmpty(); + movies.Hits.First().Original.Id.Should().NotBeEmpty(); + movies.Hits.First().Original.Name.Should().NotBeEmpty(); + movies.Hits.First().Original.Genre.Should().NotBeEmpty(); + movies.Hits.First().Formatted.Name.Should().NotBeEmpty(); } [Fact] public async Task CustomSearchWithNoQuery() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( null, new SearchQuery { AttributesToHighlight = new string[] { "name" } }); movies.Hits.Should().NotBeEmpty(); - movies.Hits.First().Id.Should().NotBeNull(); - movies.Hits.First().Name.Should().NotBeNull(); - movies.Hits.First()._Formatted.Id.Should().NotBeNull(); - movies.Hits.First()._Formatted.Name.Should().NotBeNull(); + movies.Hits.First().Original.Id.Should().NotBeNull(); + movies.Hits.First().Original.Name.Should().NotBeNull(); + movies.Hits.First().Formatted.Id.Should().NotBeNull(); + movies.Hits.First().Formatted.Name.Should().NotBeNull(); } [Fact] public async Task CustomSearchWithEmptyQuery() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( string.Empty, new SearchQuery { AttributesToHighlight = new string[] { "name" } }); movies.Hits.Should().NotBeEmpty(); - movies.Hits.First().Id.Should().NotBeNull(); - movies.Hits.First().Name.Should().NotBeNull(); - movies.Hits.First()._Formatted.Id.Should().NotBeNull(); - movies.Hits.First()._Formatted.Name.Should().NotBeNull(); + movies.Hits.First().Original.Id.Should().NotBeNull(); + movies.Hits.First().Original.Name.Should().NotBeNull(); + movies.Hits.First().Formatted.Id.Should().NotBeNull(); + movies.Hits.First().Formatted.Name.Should().NotBeNull(); } [Fact] public async Task CustomSearchWithMultipleOptions() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { @@ -158,12 +186,12 @@ public async Task CustomSearchWithMultipleOptions() Assert.NotEmpty(movies.Hits); Assert.Single(movies.Hits); - Assert.NotEmpty(firstHit.Name); - Assert.NotEmpty(firstHit.Id); - Assert.Null(firstHit.Genre); - Assert.NotEmpty(firstHit._Formatted.Name); - Assert.Equal("15", firstHit._Formatted.Id); - Assert.Null(firstHit._Formatted.Genre); + Assert.NotEmpty(firstHit.Original.Name); + Assert.NotEmpty(firstHit.Original.Id); + Assert.Null(firstHit.Original.Genre); + Assert.NotEmpty(firstHit.Formatted.Name); + Assert.Equal("15", firstHit.Formatted.Id); + Assert.Null(firstHit.Formatted.Genre); } [Fact] @@ -363,31 +391,31 @@ public async Task CustomSearchWithSort() [Fact] public async Task CustomSearchWithCroppingParameters() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { CropLength = 1, AttributesToCrop = new string[] { "*" } } ); Assert.NotEmpty(movies.Hits); - Assert.Equal("…Man", movies.Hits.First()._Formatted.Name); + Assert.Equal("…Man", movies.Hits.First().Formatted.Name); } [Fact] public async Task CustomSearchWithCropMarker() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { CropLength = 1, AttributesToCrop = new string[] { "*" }, CropMarker = "[…] " } ); Assert.NotEmpty(movies.Hits); - Assert.Equal("[…] Man", movies.Hits.First()._Formatted.Name); + Assert.Equal("[…] Man", movies.Hits.First().Formatted.Name); } [Fact] public async Task CustomSearchWithCustomHighlightTags() { - var movies = await _basicIndex.SearchAsync( + var movies = await _basicIndex.SearchAsync( "man", new SearchQuery { @@ -398,7 +426,7 @@ public async Task CustomSearchWithCustomHighlightTags() ); Assert.NotEmpty(movies.Hits); - Assert.Equal("Iron Man", movies.Hits.First()._Formatted.Name); + Assert.Equal("Iron Man", movies.Hits.First().Formatted.Name); } [Fact]