-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathFieldValue.cs
390 lines (325 loc) · 12.4 KB
/
FieldValue.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using System.Text.Json;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.Globalization;
using Elastic.Transport.Extensions;
#if ELASTICSEARCH_SERVERLESS
using Elastic.Clients.Elasticsearch.Serverless.Serialization;
#else
using Elastic.Clients.Elasticsearch.Serialization;
#endif
#if ELASTICSEARCH_SERVERLESS
namespace Elastic.Clients.Elasticsearch.Serverless;
#else
namespace Elastic.Clients.Elasticsearch;
#endif
/// <summary>
/// Represents a value for a field which depends on the field mapping and is only known at runtime,
/// therefore cannot be specifically typed.
/// </summary>
[JsonConverter(typeof(FieldValueConverter))]
public readonly struct FieldValue :
IEquatable<FieldValue>
{
internal FieldValue(ValueKind kind, object? value)
{
Kind = kind;
Value = value;
}
/// <summary>
/// The kind of value contained within this <see cref="FieldValue"/>.
/// </summary>
public readonly ValueKind Kind { get; }
/// <summary>
/// The value contained within this <see cref="FieldValue"/>.
/// </summary>
public readonly object? Value { get; }
/// <summary>
/// An enumeration of the possible value kinds that the <see cref="FieldValue"/> may contain.
/// </summary>
public enum ValueKind
{
Null,
Double,
Long,
Boolean,
String,
LazyDocument,
Composite
}
/// <summary>
/// Represents a null value.
/// </summary>
public static FieldValue Null { get; } = new(ValueKind.Null, null);
/// <summary>
/// Represents a true boolean value.
/// </summary>
public static FieldValue True { get; } = new(ValueKind.Boolean, true);
/// <summary>
/// Represents a false boolean value.
/// </summary>
public static FieldValue False { get; } = new(ValueKind.Boolean, false);
/// <summary>
/// Factory method to create a <see cref="FieldValue"/> containing a long value.
/// </summary>
/// <param name="value">The long to store as the value.</param>
/// <returns>The new <see cref="FieldValue"/>.</returns>
public static FieldValue Long(long value) => new(ValueKind.Long, value);
/// <summary>
/// Factory method to create a <see cref="FieldValue"/> containing a boolean value.
/// </summary>
/// <param name="value">The boolean to store as the value.</param>
/// <returns>The new <see cref="FieldValue"/>.</returns>
public static FieldValue Boolean(bool value) => new(ValueKind.Boolean, value);
/// <summary>
/// Factory method to create a <see cref="FieldValue"/> containing a double value.
/// </summary>
/// <param name="value">The double to store as the value.</param>
/// <returns>The new <see cref="FieldValue"/>.</returns>
public static FieldValue Double(double value) => new(ValueKind.Double, value);
/// <summary>
/// Factory method to create a <see cref="FieldValue"/> containing a string value.
/// </summary>
/// <param name="value">The string to store as the value.</param>
/// <returns>The new <see cref="FieldValue"/>.</returns>
public static FieldValue String(string value) => new(ValueKind.String, value);
// These are not expected to be required for consumer values but are used internally.
internal static FieldValue Any(LazyJson value) => new(ValueKind.LazyDocument, value);
internal static FieldValue Composite(object value) => new(ValueKind.Composite, value);
/// <summary>
/// Checks if the value of <see cref="ValueKind.String"/>.
/// </summary>
public bool IsString => Kind == ValueKind.String;
/// <summary>
/// Checks if the value of <see cref="ValueKind.Boolean"/>.
/// </summary>
public bool IsBool => Kind == ValueKind.Boolean;
/// <summary>
/// Checks if the value of <see cref="ValueKind.Long"/>.
/// </summary>
public bool IsLong => Kind == ValueKind.Long;
/// <summary>
/// Checks if the value of <see cref="ValueKind.Double"/>.
/// </summary>
public bool IsDouble => Kind == ValueKind.Double;
/// <summary>
/// Checks if the value of <see cref="ValueKind.LazyDocument"/>.
/// </summary>
public bool IsLazyDocument => Kind == ValueKind.LazyDocument;
/// <summary>
/// Checks if the value of <see cref="ValueKind.Null"/>.
/// </summary>
public bool IsNull => Kind == ValueKind.Null;
/// <summary>
/// Checks if the value of <see cref="ValueKind.Composite"/>.
/// </summary>
public bool IsComposite => Kind == ValueKind.Composite;
/// <summary>
/// Gets the value when the value kind is <see cref="ValueKind.String"/>.
/// </summary>
/// <param name="value">When this method returns, contains the value associated with this <see cref="FieldValue"/>,
/// if the value kind is <see cref="ValueKind.String"/>; otherwise, the default value for the type of the value parameter.
/// This parameter is passed uninitialized.</param>
/// <returns>True if the value is a <see cref="string"/>.</returns>
public bool TryGetString([NotNullWhen(returnValue: true)] out string? value)
{
value = null;
if (!IsString)
return false;
value = (string)Value;
return true;
}
/// <summary>
/// Gets the value when the value kind is <see cref="ValueKind.String"/>.
/// </summary>
/// <param name="value">When this method returns, contains the value associated with this <see cref="FieldValue"/>,
/// if the value kind is <see cref="ValueKind.String"/>; otherwise, the default value for the type of the value parameter.
/// This parameter is passed uninitialized.</param>
/// <returns>True if the value is a <see cref="string"/>.</returns>
public bool TryGetBool([NotNullWhen(returnValue: true)] out bool? value)
{
value = null;
if (!IsBool)
return false;
value = (bool)Value;
return true;
}
/// <summary>
/// Gets the value when the value kind is <see cref="ValueKind.Long"/>.
/// </summary>
/// <param name="value">When this method returns, contains the value associated with this <see cref="FieldValue"/>,
/// if the value kind is <see cref="ValueKind.Long"/>; otherwise, the default value for the type of the value parameter.
/// This parameter is passed uninitialized.</param>
/// <returns>True if the value is a <see cref="long"/>.</returns>
public bool TryGetLong([NotNullWhen(returnValue: true)] out long? value)
{
value = null;
if (!IsLong)
return false;
value = (long)Value;
return true;
}
/// <summary>
/// Gets the value when the value kind is <see cref="ValueKind.Double"/>.
/// </summary>
/// <param name="value">When this method returns, contains the value associated with this <see cref="FieldValue"/>,
/// if the value kind is <see cref="ValueKind.Double"/>; otherwise, the default value for the type of the value parameter.
/// This parameter is passed uninitialized.</param>
/// <returns>True if the value is a <see cref="double"/>.</returns>
public bool TryGetDouble([NotNullWhen(returnValue: true)] out double? value)
{
value = null;
if (!IsDouble)
return false;
value = (double)Value;
return true;
}
/// <summary>
/// Gets the value when the value kind is <see cref="ValueKind.LazyDocument"/>.
/// </summary>
/// <param name="value">When this method returns, contains the value associated with this <see cref="FieldValue"/>,
/// if the value kind is <see cref="ValueKind.LazyDocument"/>; otherwise, the default value for the type of the value parameter.
/// This parameter is passed uninitialized.</param>
/// <returns>True if the value is a <see cref="LazyJson"/>.</returns>
public bool TryGetLazyDocument([NotNullWhen(returnValue: true)] out LazyJson? value)
{
value = null;
if (!IsLazyDocument)
return false;
value = (LazyJson)Value;
return true;
}
/// <summary>
/// Gets the value when the value kind is <see cref="ValueKind.Composite"/> and the value type is <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type expected for the value.</typeparam>
/// <param name="value">When this method returns, contains the value associated with this <see cref="FieldValue"/>,
/// if the value kind is <see cref="ValueKind.Composite"/> and the value type is <typeparamref name="T"/>; otherwise, the default
/// value for the type of the value parameter. This parameter is passed uninitialized.</param>
/// <returns>True if the value is of the specified <typeparamref name="T"/> type.</returns>
public bool TryGet<T>([NotNullWhen(returnValue: true)] out T? value)
{
value = default;
if (!IsComposite || Value is not T typedValue)
return false;
value = typedValue;
return true;
}
internal bool TryGetComposite([NotNullWhen(returnValue: true)] out object? value)
{
value = default;
if (!IsComposite)
return false;
value = Value;
return true;
}
public override string ToString() =>
Kind switch
{
ValueKind.Null => "null",
ValueKind.Double => ((double)Value).ToString(CultureInfo.InvariantCulture),
ValueKind.Long => ((long)Value).ToString(CultureInfo.InvariantCulture),
ValueKind.Boolean => ((bool)Value).ToString(CultureInfo.InvariantCulture),
ValueKind.String => Value as string,
ValueKind.LazyDocument or ValueKind.Composite => throw new InvalidOperationException(
"Composite field value cannot be formatted as a string."),
_ => throw new InvalidOperationException($"Unknown kind '{Kind}'")
};
public override bool Equals(object? obj) => obj is FieldValue value && Equals(value);
public bool Equals(FieldValue other) => Kind == other.Kind && EqualityComparer<object?>.Default.Equals(Value, other.Value);
public override int GetHashCode()
{
unchecked
{
var hashCode = 1484969029;
hashCode = hashCode * -1521134295 + Kind.GetHashCode();
hashCode = hashCode * -1521134295 + EqualityComparer<object?>.Default.GetHashCode(Value);
return hashCode;
}
}
public static bool operator ==(FieldValue left, FieldValue right) => left.Equals(right);
public static bool operator !=(FieldValue left, FieldValue right) => !(left == right);
public static implicit operator FieldValue(string value) => String(value);
public static implicit operator FieldValue(bool value) => Boolean(value);
public static implicit operator FieldValue(int value) => Long(value);
public static implicit operator FieldValue(long value) => Long(value);
public static implicit operator FieldValue(double value) => Double(value);
}
internal sealed class FieldValueConverter : JsonConverter<FieldValue>
{
public override FieldValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return FieldValue.Null;
case JsonTokenType.String:
var stringValue = reader.GetString();
return FieldValue.String(stringValue);
case JsonTokenType.Number:
if (reader.TryGetInt64(out var l))
{
return FieldValue.Long(l);
}
else if (reader.TryGetDouble(out var d))
{
return FieldValue.Double(d);
}
else
{
throw new JsonException("Unexpected number format which cannot be deserialised as a FieldValue.");
}
case JsonTokenType.True:
return FieldValue.True;
case JsonTokenType.False:
return FieldValue.False;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
var value = JsonSerializer.Deserialize<LazyJson>(ref reader, options);
return FieldValue.Any(value);
}
throw new JsonException($"Unexpected token type '{reader.TokenType}' read while deserializing a FieldValue.");
}
public override void Write(Utf8JsonWriter writer, FieldValue value, JsonSerializerOptions options)
{
if (value.TryGetString(out var stringValue))
{
writer.WriteStringValue(stringValue);
}
else if (value.TryGetBool(out var boolValue))
{
writer.WriteBooleanValue(boolValue.Value);
}
else if (value.TryGetLong(out var longValue))
{
writer.WriteNumberValue(longValue.Value);
}
else if (value.TryGetDouble(out var doubleValue))
{
writer.WriteNumberValue(doubleValue.Value);
}
else if (value.TryGetLazyDocument(out var lazyDocument))
{
writer.WriteRawValue(lazyDocument.Value.Bytes);
}
else if (value.TryGetComposite(out var objectValue))
{
if (!options.TryGetClientSettings(out var settings))
ThrowHelper.ThrowJsonExceptionForMissingSettings();
settings.SourceSerializer.Serialize(objectValue, objectValue.GetType(), writer, null);
}
else if (value.Kind == FieldValue.ValueKind.Null)
{
writer.WriteNullValue();
}
else
{
throw new JsonException($"Unsupported FieldValue kind. This is likely a bug and should be reported as an issue.");
}
}
}