Skip to content

Commit 7e61039

Browse files
Support dictionary responses (#6874) (#6879)
* Support dictionary responses Includes improvements and fixes for Properties serialization * Fix BOM Co-authored-by: Steve Gordon <sgordon@hotmail.co.uk>
1 parent d45c863 commit 7e61039

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1316
-761
lines changed

.github/workflows/integration-jobs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ jobs:
2626
'8.2.3',
2727
'8.3.3',
2828
'8.4.3',
29-
'8.5.0-SNAPSHOT',
29+
"8.5.0",
30+
'8.6.0-SNAPSHOT',
3031
'latest-8'
3132
]
3233

build/scripts/Testing.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module Tests =
4646
sprintf "tests/%s.runsettings" prefix
4747

4848
Directory.CreateDirectory Paths.BuildOutput |> ignore
49-
let command = ["test"; proj; "--nologo"; "-c"; "Release"; "-s"; runSettings; "--no-build"]
49+
let command = ["test"; proj; "--nologo"; "-c"; "Release"; "-s"; runSettings; "--no-build"; "--blame"]
5050

5151
let wantsTrx =
5252
let wants = match args.CommandArguments with | Integration a -> a.TrxExport | Test t -> t.TrxExport | _ -> false
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using Elastic.Clients.Elasticsearch.Mapping;
7+
8+
namespace Elastic.Clients.Elasticsearch.IndexManagement;
9+
10+
public partial class MappingResponse
11+
{
12+
public IReadOnlyDictionary<IndexName, IndexMappingRecord> Indices => BackingDictionary;
13+
}
14+
15+
public static class GetMappingResponseExtensions
16+
{
17+
public static TypeMapping GetMappingFor<T>(this MappingResponse response) => response.GetMappingFor(typeof(T));
18+
19+
public static TypeMapping GetMappingFor(this MappingResponse response, IndexName index)
20+
{
21+
if (index.IsNullOrEmpty())
22+
return null;
23+
24+
return response.Indices.TryGetValue(index, out var indexMappings) ? indexMappings.Mappings : null;
25+
}
26+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Runtime.CompilerServices;
7+
using System.Text.Json;
8+
9+
namespace Elastic.Clients.Elasticsearch;
10+
11+
internal static class ThrowHelper
12+
{
13+
[MethodImpl(MethodImplOptions.NoInlining)]
14+
internal static void ThrowJsonException(string? message = null) => throw new JsonException(message);
15+
16+
[MethodImpl(MethodImplOptions.NoInlining)]
17+
internal static void ThrowUnknownTaggedUnionVariantJsonException(string variantTag, Type interfaceType) =>
18+
throw new JsonException($"Encounted an unsupported variant tag '{variantTag}' on '{SimplifiedFullName(interfaceType)}', which could not be deserialised.");
19+
20+
[MethodImpl(MethodImplOptions.NoInlining)]
21+
private static string SimplifiedFullName(Type type) => type.FullName.Substring(30);
22+
}

src/Elastic.Clients.Elasticsearch/Core/Infer/IndexName/IndexName.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,13 @@ private bool EqualsMarker(IndexName other)
123123
{
124124
if (other == null)
125125
return false;
126+
126127
if (!Name.IsNullOrEmpty() && !other.Name.IsNullOrEmpty())
127128
return EqualsString(PrefixClusterName(other, other.Name));
128129

129130
if ((!Cluster.IsNullOrEmpty() || !other.Cluster.IsNullOrEmpty()) && Cluster != other.Cluster)
130131
return false;
131132

132-
return Type != null && other?.Type != null && Type == other.Type;
133+
return Type is not null && other?.Type is not null && Type == other.Type;
133134
}
134135
}

src/Elastic.Clients.Elasticsearch/Core/IsAReadOnlyDictionary.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@ internal IsAReadOnlyDictionary(IReadOnlyDictionary<TKey, TValue> backingDictiona
1919
return;
2020

2121
var dictionary = new Dictionary<TKey, TValue>(backingDictionary.Count);
22+
2223
foreach (var key in backingDictionary.Keys)
23-
// ReSharper disable once VirtualMemberCallInConstructor
24-
// expect all implementations of Sanitize to be pure
2524
dictionary[Sanitize(key)] = backingDictionary[key];
2625

2726
BackingDictionary = dictionary;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using Elastic.Transport.Products.Elasticsearch;
8+
9+
namespace Elastic.Clients.Elasticsearch;
10+
11+
public abstract class DictionaryResponse<TKey, TValue> : ElasticsearchResponseBase
12+
{
13+
internal DictionaryResponse(IReadOnlyDictionary<TKey, TValue> dictionary)
14+
{
15+
if (dictionary is null)
16+
throw new ArgumentNullException(nameof(dictionary));
17+
18+
BackingDictionary = dictionary;
19+
}
20+
21+
internal DictionaryResponse() => BackingDictionary = EmptyReadOnly<TKey, TValue>.Dictionary;
22+
23+
protected IReadOnlyDictionary<TKey, TValue> BackingDictionary { get; }
24+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Elastic.Transport;
6+
using System.Collections;
7+
using System.Collections.Generic;
8+
9+
namespace Elastic.Clients.Elasticsearch;
10+
11+
/// <summary>
12+
/// A proxy dictionary that is settings-aware to correctly handle IUrlParameter-based keys such as IndexName.
13+
/// </summary>
14+
public sealed class ResolvableDictionaryProxy<TKey, TValue> : IIsAReadOnlyDictionary<TKey, TValue>
15+
where TKey : IUrlParameter
16+
{
17+
private readonly IElasticsearchClientSettings _elasticsearchClientSettings;
18+
19+
internal ResolvableDictionaryProxy(IElasticsearchClientSettings elasticsearchClientSettings, IReadOnlyDictionary<TKey, TValue> backingDictionary)
20+
{
21+
_elasticsearchClientSettings = elasticsearchClientSettings;
22+
23+
if (backingDictionary == null)
24+
return;
25+
26+
Original = backingDictionary;
27+
28+
var dictionary = new Dictionary<string, TValue>(backingDictionary.Count);
29+
30+
foreach (var key in backingDictionary.Keys)
31+
dictionary[Sanitize(key)] = backingDictionary[key];
32+
33+
BackingDictionary = dictionary;
34+
}
35+
36+
public int Count => BackingDictionary.Count;
37+
38+
public TValue this[TKey key] => BackingDictionary.TryGetValue(Sanitize(key), out var v) ? v : default;
39+
public TValue this[string key] => BackingDictionary.TryGetValue(key, out var v) ? v : default;
40+
41+
public IEnumerable<TKey> Keys => Original.Keys;
42+
public IEnumerable<string> ResolvedKeys => BackingDictionary.Keys;
43+
44+
public IEnumerable<TValue> Values => BackingDictionary.Values;
45+
internal IReadOnlyDictionary<string, TValue> BackingDictionary { get; } = EmptyReadOnly<string, TValue>.Dictionary;
46+
private IReadOnlyDictionary<TKey, TValue> Original { get; } = EmptyReadOnly<TKey, TValue>.Dictionary;
47+
48+
IEnumerator IEnumerable.GetEnumerator() => Original.GetEnumerator();
49+
50+
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() =>
51+
Original.GetEnumerator();
52+
53+
public bool ContainsKey(TKey key) => BackingDictionary.ContainsKey(Sanitize(key));
54+
55+
public bool TryGetValue(TKey key, out TValue value) =>
56+
BackingDictionary.TryGetValue(Sanitize(key), out value);
57+
58+
private string Sanitize(TKey key) => key?.GetString(_elasticsearchClientSettings);
59+
}

src/Elastic.Clients.Elasticsearch/Serialization/DefaultRequestResponseSerializer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5-
using System;
65
using System.IO;
76
using System.Text.Json;
87
using System.Text.Json.Serialization;
@@ -47,10 +46,11 @@ public DefaultRequestResponseSerializer(IElasticsearchClientSettings settings)
4746
new SelfTwoWaySerializableConverterFactory(settings),
4847
new IndicesJsonConverter(settings),
4948
new IdsConverter(settings),
50-
new IsADictionaryConverter(),
49+
new IsADictionaryConverterFactory(),
5150
new ResponseItemConverterFactory(),
5251
new UnionConverter(),
53-
new ExtraSerializationData(settings)
52+
new ExtraSerializationData(settings),
53+
new DictionaryResponseConverterFactory(settings)
5454
},
5555
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
5656
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
using Elastic.Transport;
10+
11+
namespace Elastic.Clients.Elasticsearch.Serialization;
12+
13+
internal sealed class DictionaryResponseConverterFactory : JsonConverterFactory
14+
{
15+
private readonly IElasticsearchClientSettings _settings;
16+
17+
public DictionaryResponseConverterFactory(IElasticsearchClientSettings settings) => _settings = settings;
18+
19+
public override bool CanConvert(Type typeToConvert) =>
20+
typeToConvert.BaseType is not null &&
21+
typeToConvert.BaseType.IsGenericType &&
22+
typeToConvert.BaseType.GetGenericTypeDefinition() == typeof(DictionaryResponse<,>);
23+
24+
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
25+
{
26+
var args = typeToConvert.BaseType.GetGenericArguments();
27+
28+
var keyType = args[0];
29+
var valueType = args[1];
30+
31+
if (keyType.IsClass)
32+
{
33+
if (keyType == typeof(IndexName))
34+
{
35+
return (JsonConverter)Activator.CreateInstance(
36+
typeof(ResolvableDictionaryResponseConverterInner<,,>).MakeGenericType(typeToConvert, keyType, valueType), _settings);
37+
}
38+
39+
return (JsonConverter)Activator.CreateInstance(
40+
typeof(DictionaryResponseConverterInner<,,>).MakeGenericType(typeToConvert, keyType, valueType));
41+
}
42+
43+
return null;
44+
}
45+
46+
private class DictionaryResponseConverterInner<TType, TKey, TValue> : JsonConverter<TType>
47+
where TKey : class
48+
where TType : DictionaryResponse<TKey, TValue>, new()
49+
{
50+
public override TType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
51+
{
52+
var dictionary = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(ref reader, options);
53+
54+
if (dictionary is null)
55+
return null;
56+
57+
return (TType)Activator.CreateInstance(typeof(TType), new object[] { dictionary });
58+
}
59+
60+
public override void Write(Utf8JsonWriter writer, TType value, JsonSerializerOptions options) =>
61+
throw new NotImplementedException("Response converters do not support serialization.");
62+
}
63+
64+
private class ResolvableDictionaryResponseConverterInner<TType, TKey, TValue> : JsonConverter<TType>
65+
where TKey : class, IUrlParameter
66+
where TType : DictionaryResponse<TKey, TValue>, new()
67+
{
68+
private readonly IElasticsearchClientSettings _settings;
69+
70+
public ResolvableDictionaryResponseConverterInner(IElasticsearchClientSettings settings) => _settings = settings;
71+
72+
public override TType? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
73+
{
74+
var dictionary = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(ref reader, options);
75+
76+
if (dictionary is null)
77+
return null;
78+
79+
var dictionaryProxy = new ResolvableDictionaryProxy<TKey, TValue>(_settings, dictionary);
80+
81+
return (TType)Activator.CreateInstance(typeof(TType), new object[] { dictionaryProxy });
82+
}
83+
84+
public override void Write(Utf8JsonWriter writer, TType value, JsonSerializerOptions options) =>
85+
throw new NotImplementedException("Response converters do not support serialization.");
86+
}
87+
}

src/Elastic.Clients.Elasticsearch/Serialization/ThrowHelper.cs renamed to src/Elastic.Clients.Elasticsearch/Serialization/IUnionVerifiable.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,9 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5-
using System.Runtime.CompilerServices;
6-
using System.Text.Json;
7-
85
namespace Elastic.Clients.Elasticsearch.Serialization;
96

10-
internal class ThrowHelper
7+
internal interface IUnionVerifiable
118
{
12-
[MethodImpl(MethodImplOptions.NoInlining)]
13-
public static void ThrowJsonException(string? message = null) => throw new JsonException(message);
9+
bool IsSuccessful { get; }
1410
}

src/Elastic.Clients.Elasticsearch/Serialization/IsADictionaryConverter.cs renamed to src/Elastic.Clients.Elasticsearch/Serialization/IsADictionaryConverterFactory.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
using System.Collections.Generic;
77
using System.Text.Json;
88
using System.Text.Json.Serialization;
9+
using Elastic.Clients.Elasticsearch.Mapping;
910

1011
namespace Elastic.Clients.Elasticsearch.Serialization;
1112

12-
// TODO : We need to handle these cases https://github.com/elastic/elasticsearch-specification/pull/1589
13-
14-
internal sealed class IsADictionaryConverter : JsonConverterFactory
13+
internal sealed class IsADictionaryConverterFactory : JsonConverterFactory
1514
{
1615
public override bool CanConvert(Type typeToConvert) =>
16+
typeToConvert.Name != nameof(Properties) && // Properties has it's own converter assigned
1717
typeToConvert.BaseType is not null &&
1818
typeToConvert.BaseType.IsGenericType &&
1919
typeToConvert.BaseType.GetGenericTypeDefinition() == typeof(IsADictionary<,>);
@@ -52,8 +52,3 @@ public override void Write(Utf8JsonWriter writer, TType value, JsonSerializerOpt
5252
JsonSerializer.Serialize<Dictionary<TKey, TValue>>(writer, value.BackingDictionary, options);
5353
}
5454
}
55-
56-
internal interface IUnionVerifiable
57-
{
58-
bool IsSuccessful { get; }
59-
}

0 commit comments

Comments
 (0)