• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Jericho / ZoomNet / 643

22 Sep 2023 01:19AM UTC coverage: 17.818% (+0.3%) from 17.563%
643

push

appveyor

Jericho
Merge branch 'release/0.67.0'

526 of 2952 relevant lines covered (17.82%)

2.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

48.11
/Source/ZoomNet/Extensions/Internal.cs
1
using HttpMultipartParser;
2
using Pathoschild.Http.Client;
3
using System;
4
using System.Collections;
5
using System.Collections.Generic;
6
using System.ComponentModel;
7
using System.Globalization;
8
using System.IO;
9
using System.IO.Compression;
10
using System.Linq;
11
using System.Net.Http;
12
using System.Net.Http.Headers;
13
using System.Reflection;
14
using System.Runtime.Serialization;
15
using System.Text;
16
using System.Text.Json;
17
using System.Text.Json.Nodes;
18
using System.Text.Json.Serialization;
19
using System.Threading;
20
using System.Threading.Tasks;
21
using ZoomNet.Json;
22
using ZoomNet.Models;
23
using ZoomNet.Utilities;
24
using static ZoomNet.Utilities.DiagnosticHandler;
25

26
namespace ZoomNet
27
{
28
        /// <summary>
29
        /// Internal extension methods.
30
        /// </summary>
31
        internal static class Internal
32
        {
33
                internal enum UnixTimePrecision
34
                {
35
                        Seconds = 0,
36
                        Milliseconds = 1
37
                }
38

39
                private static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
1✔
40

41
                /// <summary>
42
                /// Converts a 'unix time', which is expressed as the number of seconds (or milliseconds) since
43
                /// midnight on January 1st 1970, to a .Net <see cref="DateTime" />.
44
                /// </summary>
45
                /// <param name="unixTime">The unix time.</param>
46
                /// <param name="precision">The precision of the provided unix time.</param>
47
                /// <returns>
48
                /// The <see cref="DateTime" />.
49
                /// </returns>
50
                internal static DateTime FromUnixTime(this long unixTime, UnixTimePrecision precision = UnixTimePrecision.Seconds)
51
                {
52
                        if (precision == UnixTimePrecision.Seconds) return EPOCH.AddSeconds(unixTime);
12✔
53
                        if (precision == UnixTimePrecision.Milliseconds) return EPOCH.AddMilliseconds(unixTime);
24✔
54
                        throw new Exception($"Unknown precision: {precision}");
×
55
                }
56

57
                /// <summary>
58
                /// Converts a .Net <see cref="DateTime" /> into a 'Unix time', which is expressed as the number
59
                /// of seconds (or milliseconds) since midnight on January 1st 1970.
60
                /// </summary>
61
                /// <param name="date">The date.</param>
62
                /// <param name="precision">The desired precision.</param>
63
                /// <returns>
64
                /// The numer of seconds/milliseconds since midnight on January 1st 1970.
65
                /// </returns>
66
                internal static long ToUnixTime(this DateTime date, UnixTimePrecision precision = UnixTimePrecision.Seconds)
67
                {
68
                        var diff = date.ToUniversalTime() - EPOCH;
×
69
                        if (precision == UnixTimePrecision.Seconds) return Convert.ToInt64(diff.TotalSeconds);
×
70
                        if (precision == UnixTimePrecision.Milliseconds) return Convert.ToInt64(diff.TotalMilliseconds);
×
71
                        throw new Exception($"Unknown precision: {precision}");
×
72
                }
73

74
                /// <summary>
75
                /// Converts a .Net <see cref="DateTime" /> into a string that can be accepted by the Zoom API.
76
                /// </summary>
77
                /// <param name="date">The date.</param>
78
                /// <param name="timeZone">The time zone.</param>
79
                /// <param name="dateOnly">Indicates if you want only the date to be converted to a string (ignoring the time).</param>
80
                /// <returns>
81
                /// The string representation of the date expressed in the Zoom format.
82
                /// </returns>
83
                internal static string ToZoomFormat(this DateTime? date, TimeZones? timeZone = null, bool dateOnly = false)
84
                {
85
                        if (!date.HasValue) return null;
×
86
                        return date.Value.ToZoomFormat(timeZone, dateOnly);
×
87
                }
88

89
                /// <summary>
90
                /// Converts a .Net <see cref="DateTime" /> into a string that can be accepted by the Zoom API.
91
                /// </summary>
92
                /// <param name="date">The date.</param>
93
                /// <param name="timeZone">The time zone.</param>
94
                /// <param name="dateOnly">Indicates if you want only the date to be converted to a string (ignoring the time).</param>
95
                /// <returns>
96
                /// The string representation of the date expressed in the Zoom format.
97
                /// </returns>
98
                internal static string ToZoomFormat(this DateTime date, TimeZones? timeZone = null, bool dateOnly = false)
99
                {
100
                        const string dateOnlyFormat = "yyyy-MM-dd";
101
                        const string defaultDateFormat = dateOnlyFormat + "'T'HH:mm:ss";
102
                        const string utcDateFormat = defaultDateFormat + "'Z'";
103

104
                        if (dateOnly)
2✔
105
                        {
106
                                if (timeZone.HasValue && timeZone.Value == TimeZones.UTC) return date.ToUniversalTime().ToString(dateOnlyFormat);
2✔
107
                                else return date.ToString(dateOnlyFormat);
2✔
108
                        }
109
                        else
110
                        {
111
                                if (timeZone.HasValue && timeZone.Value == TimeZones.UTC) return date.ToUniversalTime().ToString(utcDateFormat);
×
112
                                else return date.ToString(defaultDateFormat);
×
113
                        }
114
                }
115

116
                /// <summary>
117
                /// Reads the content of the HTTP response as string asynchronously.
118
                /// </summary>
119
                /// <param name="httpContent">The content.</param>
120
                /// <param name="encoding">The encoding. You can leave this parameter null and the encoding will be
121
                /// automatically calculated based on the charset in the response. Also, UTF-8
122
                /// encoding will be used if the charset is absent from the response, is blank
123
                /// or contains an invalid value.</param>
124
                /// <param name="cancellationToken">The cancellation token.</param>
125
                /// <returns>The string content of the response.</returns>
126
                /// <remarks>
127
                /// This method is an improvement over the built-in ReadAsStringAsync method
128
                /// because it can handle invalid charset returned in the response. For example
129
                /// you may be sending a request to an API that returns a blank charset or a
130
                /// misspelled one like 'utf8' instead of the correctly spelled 'utf-8'. The
131
                /// built-in method throws an exception if an invalid charset is specified
132
                /// while this method uses the UTF-8 encoding in that situation.
133
                ///
134
                /// My motivation for writing this extension method was to work around a situation
135
                /// where the 3rd party API I was sending requests to would sometimes return 'utf8'
136
                /// as the charset and an exception would be thrown when I called the ReadAsStringAsync
137
                /// method to get the content of the response into a string because the .Net HttpClient
138
                /// would attempt to determine the proper encoding to use but it would fail due to
139
                /// the fact that the charset was misspelled. I contacted the vendor, asking them
140
                /// to either omit the charset or fix the misspelling but they didn't feel the need
141
                /// to fix this issue because:
142
                /// "in some programming languages, you can use the syntax utf8 instead of utf-8".
143
                /// In other words, they are happy to continue using the misspelled value which is
144
                /// supported by "some" programming languages instead of using the properly spelled
145
                /// value which is supported by all programming languages.
146
                /// </remarks>
147
                /// <example>
148
                /// <code>
149
                /// var httpRequest = new HttpRequestMessage
150
                /// {
151
                ///     Method = HttpMethod.Get,
152
                ///     RequestUri = new Uri("https://api.vendor.com/v1/endpoint")
153
                /// };
154
                /// var httpClient = new HttpClient();
155
                /// var response = await httpClient.SendAsync(httpRequest, CancellationToken.None).ConfigureAwait(false);
156
                /// var responseContent = await response.Content.ReadAsStringAsync(null).ConfigureAwait(false);
157
                /// </code>
158
                /// </example>
159
                internal static async Task<string> ReadAsStringAsync(this HttpContent httpContent, Encoding encoding, CancellationToken cancellationToken = default)
160
                {
161
                        var content = string.Empty;
162

163
                        if (httpContent != null)
164
                        {
165
#if NET5_0_OR_GREATER
166
                                var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
167
#else
168
                                var contentStream = await httpContent.ReadAsStreamAsync().ConfigureAwait(false);
169
#endif
170
                                encoding ??= httpContent.GetEncoding(Encoding.UTF8);
171

172
                                // This is important: we must make a copy of the response stream otherwise we would get an
173
                                // exception on subsequent attempts to read the content of the stream
174
                                using (var ms = Utils.MemoryStreamManager.GetStream())
175
                                {
176
                                        const int DefaultBufferSize = 81920;
177
                                        await contentStream.CopyToAsync(ms, DefaultBufferSize, cancellationToken).ConfigureAwait(false);
178
                                        ms.Position = 0;
179
                                        using (var sr = new StreamReader(ms, encoding))
180
                                        {
181
#if NET7_0_OR_GREATER
182
                                                content = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
183
#else
184
                                                content = await sr.ReadToEndAsync().ConfigureAwait(false);
185
#endif
186
                                        }
187

188
                                        // It's important to rewind the stream
189
                                        if (contentStream.CanSeek) contentStream.Position = 0;
190
                                }
191
                        }
192

193
                        return content;
194
                }
195

196
                /// <summary>
197
                /// Gets the encoding.
198
                /// </summary>
199
                /// <param name="content">The content.</param>
200
                /// <param name="defaultEncoding">The default encoding.</param>
201
                /// <returns>
202
                /// The encoding.
203
                /// </returns>
204
                /// <remarks>
205
                /// This method tries to get the encoding based on the charset or uses the
206
                /// 'defaultEncoding' if the charset is empty or contains an invalid value.
207
                /// </remarks>
208
                /// <example>
209
                ///   <code>
210
                /// var httpRequest = new HttpRequestMessage
211
                /// {
212
                /// Method = HttpMethod.Get,
213
                /// RequestUri = new Uri("https://my.api.com/v1/myendpoint")
214
                /// };
215
                /// var httpClient = new HttpClient();
216
                /// var response = await httpClient.SendAsync(httpRequest, CancellationToken.None).ConfigureAwait(false);
217
                /// var encoding = response.Content.GetEncoding(Encoding.UTF8);
218
                /// </code>
219
                /// </example>
220
                internal static Encoding GetEncoding(this HttpContent content, Encoding defaultEncoding)
221
                {
222
                        var encoding = defaultEncoding;
17✔
223
                        try
224
                        {
225
                                var charset = content?.Headers?.ContentType?.CharSet;
17✔
226
                                if (!string.IsNullOrEmpty(charset))
17✔
227
                                {
228
                                        encoding = Encoding.GetEncoding(charset);
17✔
229
                                }
230
                        }
17✔
231
                        catch
×
232
                        {
233
                                encoding = defaultEncoding;
×
234
                        }
×
235

236
                        return encoding;
17✔
237
                }
238

239
                /// <summary>
240
                /// Returns the value of a parameter or the default value if it doesn't exist.
241
                /// </summary>
242
                /// <param name="parser">The parser.</param>
243
                /// <param name="name">The name of the parameter.</param>
244
                /// <param name="defaultValue">The default value.</param>
245
                /// <returns>The value of the parameter.</returns>
246
                internal static string GetParameterValue(this MultipartFormDataParser parser, string name, string defaultValue)
247
                {
248
                        if (parser.HasParameter(name)) return parser.GetParameterValue(name);
×
249
                        else return defaultValue;
×
250
                }
251

252
                /// <summary>Asynchronously retrieve the JSON encoded response body and convert it to an object of the desired type.</summary>
253
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
254
                /// <param name="response">The response.</param>
255
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
256
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
257
                /// <param name="options">Options to control behavior Converter during parsing.</param>
258
                /// <returns>Returns the strongly typed object.</returns>
259
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
260
                internal static Task<T> AsObject<T>(this IResponse response, string propertyName = null, bool throwIfPropertyIsMissing = true, JsonSerializerOptions options = null)
261
                {
262
                        return response.Message.Content.AsObject<T>(propertyName, throwIfPropertyIsMissing, options);
2✔
263
                }
264

265
                /// <summary>Asynchronously retrieve the JSON encoded response body and convert it to an object of the desired type.</summary>
266
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
267
                /// <param name="request">The request.</param>
268
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
269
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
270
                /// <param name="options">Options to control behavior Converter during parsing.</param>
271
                /// <returns>Returns the strongly typed object.</returns>
272
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
273
                internal static async Task<T> AsObject<T>(this IRequest request, string propertyName = null, bool throwIfPropertyIsMissing = true, JsonSerializerOptions options = null)
274
                {
275
                        var response = await request.AsResponse().ConfigureAwait(false);
276
                        return await response.AsObject<T>(propertyName, throwIfPropertyIsMissing, options).ConfigureAwait(false);
277
                }
278

279
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponse' object.</summary>
280
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
281
                /// <param name="response">The response.</param>
282
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
283
                /// <param name="options">Options to control behavior Converter during parsing.</param>
284
                /// <returns>Returns the paginated response.</returns>
285
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
286
                internal static Task<PaginatedResponse<T>> AsPaginatedResponse<T>(this IResponse response, string propertyName = null, JsonSerializerOptions options = null)
287
                {
288
                        return response.Message.Content.AsPaginatedResponse<T>(propertyName, options);
2✔
289
                }
290

291
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponse' object.</summary>
292
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
293
                /// <param name="request">The request.</param>
294
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
295
                /// <param name="options">Options to control behavior Converter during parsing.</param>
296
                /// <returns>Returns the paginated response.</returns>
297
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
298
                internal static async Task<PaginatedResponse<T>> AsPaginatedResponse<T>(this IRequest request, string propertyName = null, JsonSerializerOptions options = null)
299
                {
300
                        var response = await request.AsResponse().ConfigureAwait(false);
301
                        return await response.AsPaginatedResponse<T>(propertyName, options).ConfigureAwait(false);
302
                }
303

304
                /// <summary>Asynchronously retrieve the JSON encoded response body and convert it to a 'PaginatedResponseWithToken' object.</summary>
305
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
306
                /// <param name="response">The response.</param>
307
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
308
                /// <param name="options">Options to control behavior Converter during parsing.</param>
309
                /// <returns>Returns the paginated response.</returns>
310
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
311
                internal static Task<PaginatedResponseWithToken<T>> AsPaginatedResponseWithToken<T>(this IResponse response, string propertyName, JsonSerializerOptions options = null)
312
                {
313
                        return response.Message.Content.AsPaginatedResponseWithToken<T>(propertyName, options);
2✔
314
                }
315

316
                /// <summary>Asynchronously retrieve the JSON encoded response body and convert it to a 'PaginatedResponseWithToken' object.</summary>
317
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
318
                /// <param name="request">The request.</param>
319
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
320
                /// <param name="options">Options to control behavior Converter during parsing.</param>
321
                /// <returns>Returns the paginated response.</returns>
322
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
323
                internal static async Task<PaginatedResponseWithToken<T>> AsPaginatedResponseWithToken<T>(this IRequest request, string propertyName, JsonSerializerOptions options = null)
324
                {
325
                        var response = await request.AsResponse().ConfigureAwait(false);
326
                        return await response.AsPaginatedResponseWithToken<T>(propertyName, options).ConfigureAwait(false);
327
                }
328

329
                /// <summary>Asynchronously retrieve the JSON encoded response body and convert it to a 'PaginatedResponseWithToken' object.</summary>
330
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
331
                /// <param name="response">The response.</param>
332
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
333
                /// <param name="options">Options to control behavior Converter during parsing.</param>
334
                /// <returns>Returns the paginated response.</returns>
335
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
336
                internal static Task<PaginatedResponseWithTokenAndDateRange<T>> AsPaginatedResponseWithTokenAndDateRange<T>(this IResponse response, string propertyName, JsonSerializerOptions options = null)
337
                {
338
                        return response.Message.Content.AsPaginatedResponseWithTokenAndDateRange<T>(propertyName, options);
3✔
339
                }
340

341
                /// <summary>Asynchronously retrieve the JSON encoded response body and convert it to a 'PaginatedResponseWithToken' object.</summary>
342
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
343
                /// <param name="request">The request.</param>
344
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
345
                /// <param name="options">Options to control behavior Converter during parsing.</param>
346
                /// <returns>Returns the paginated response.</returns>
347
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
348
                internal static async Task<PaginatedResponseWithTokenAndDateRange<T>> AsPaginatedResponseWithTokenAndDateRange<T>(this IRequest request, string propertyName, JsonSerializerOptions options = null)
349
                {
350
                        var response = await request.AsResponse().ConfigureAwait(false);
351
                        return await response.AsPaginatedResponseWithTokenAndDateRange<T>(propertyName, options).ConfigureAwait(false);
352
                }
353

354
                /// <summary>Get a raw JSON document representation of the response.</summary>
355
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
356
                internal static Task<JsonDocument> AsRawJsonDocument(this IResponse response, string propertyName = null, bool throwIfPropertyIsMissing = true)
357
                {
358
                        return response.Message.Content.AsRawJsonDocument(propertyName, throwIfPropertyIsMissing);
×
359
                }
360

361
                /// <summary>Get a raw JSON document representation of the response.</summary>
362
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
363
                internal static async Task<JsonDocument> AsRawJsonDocument(this IRequest request, string propertyName = null, bool throwIfPropertyIsMissing = true)
364
                {
365
                        var response = await request.AsResponse().ConfigureAwait(false);
366
                        return await response.AsRawJsonDocument(propertyName, throwIfPropertyIsMissing).ConfigureAwait(false);
367
                }
368

369
                /// <summary>
370
                /// Replace the current error handler which treats HTTP200 as success with a handler that treats HTTP200 as failure.
371
                /// </summary>
372
                /// <param name="request">The request.</param>
373
                /// <param name="customExceptionMessage">An optional custom error message.</param>
374
                /// <returns>Returns the request builder for chaining.</returns>
375
                internal static IRequest WithHttp200TreatedAsFailure(this IRequest request, string customExceptionMessage = null)
376
                {
377
                        return request
×
378
                                .WithoutFilter<ZoomErrorHandler>()
×
379
                                .WithFilter(new ZoomErrorHandler(true, customExceptionMessage));
×
380
                }
381

382
                /// <summary>Set the body content of the HTTP request.</summary>
383
                /// <typeparam name="T">The type of object to serialize into a JSON string.</typeparam>
384
                /// <param name="request">The request.</param>
385
                /// <param name="body">The value to serialize into the HTTP body content.</param>
386
                /// <param name="omitCharSet">Indicates if the charset should be omitted from the 'Content-Type' request header.</param>
387
                /// <returns>Returns the request builder for chaining.</returns>
388
                /// <remarks>
389
                /// This method is equivalent to IRequest.AsBody&lt;T&gt;(T body) because omitting the media type
390
                /// causes the first formatter in MediaTypeFormatterCollection to be used by default and the first
391
                /// formatter happens to be the JSON formatter. However, I don't feel good about relying on the
392
                /// default ordering of the items in the MediaTypeFormatterCollection.
393
                /// </remarks>
394
                internal static IRequest WithJsonBody<T>(this IRequest request, T body, bool omitCharSet = false)
395
                {
396
                        return request.WithBody(bodyBuilder =>
×
397
                        {
×
398
                                var httpContent = bodyBuilder.Model(body, new MediaTypeHeaderValue("application/json"));
×
399

×
400
                                if (omitCharSet && !string.IsNullOrEmpty(httpContent.Headers.ContentType.CharSet))
×
401
                                {
×
402
                                        httpContent.Headers.ContentType.CharSet = string.Empty;
×
403
                                }
×
404

×
405
                                return httpContent;
×
406
                        });
×
407
                }
408

409
                /// <summary>Asynchronously retrieve the response body as a <see cref="string"/>.</summary>
410
                /// <param name="response">The response.</param>
411
                /// <param name="encoding">The encoding. You can leave this parameter null and the encoding will be
412
                /// automatically calculated based on the charset in the response. Also, UTF-8
413
                /// encoding will be used if the charset is absent from the response, is blank
414
                /// or contains an invalid value.</param>
415
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
416
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
417
                internal static Task<string> AsString(this IResponse response, Encoding encoding)
418
                {
419
                        return response.Message.Content.ReadAsStringAsync(encoding);
×
420
                }
421

422
                /// <summary>Asynchronously retrieve the response body as a <see cref="string"/>.</summary>
423
                /// <param name="request">The request.</param>
424
                /// <param name="encoding">The encoding. You can leave this parameter null and the encoding will be
425
                /// automatically calculated based on the charset in the response. Also, UTF-8
426
                /// encoding will be used if the charset is absent from the response, is blank
427
                /// or contains an invalid value.</param>
428
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
429
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
430
                internal static async Task<string> AsString(this IRequest request, Encoding encoding)
431
                {
432
                        IResponse response = await request.AsResponse().ConfigureAwait(false);
433
                        return await response.AsString(encoding).ConfigureAwait(false);
434
                }
435

436
                /// <summary>
437
                ///  Converts the value of the current System.TimeSpan object to its equivalent string
438
                ///  representation by using a human readable format.
439
                /// </summary>
440
                /// <param name="timeSpan">The time span.</param>
441
                /// <returns>Returns the human readable representation of the TimeSpan.</returns>
442
                internal static string ToDurationString(this TimeSpan timeSpan)
443
                {
444
                        static void AppendFormatIfNecessary(StringBuilder stringBuilder, string timePart, int value)
445
                        {
446
                                if (value <= 0) return;
447
                                stringBuilder.AppendFormat($" {value} {timePart}{(value > 1 ? "s" : string.Empty)}");
448
                        }
449

450
                        // In case the TimeSpan is extremely short
451
                        if (timeSpan.TotalMilliseconds <= 1) return "1 millisecond";
3✔
452

453
                        var result = new StringBuilder();
3✔
454
                        AppendFormatIfNecessary(result, "day", timeSpan.Days);
3✔
455
                        AppendFormatIfNecessary(result, "hour", timeSpan.Hours);
3✔
456
                        AppendFormatIfNecessary(result, "minute", timeSpan.Minutes);
3✔
457
                        AppendFormatIfNecessary(result, "second", timeSpan.Seconds);
3✔
458
                        AppendFormatIfNecessary(result, "millisecond", timeSpan.Milliseconds);
3✔
459
                        return result.ToString().Trim();
3✔
460
                }
461

462
                /// <summary>
463
                /// Ensure that a string starts with a given prefix.
464
                /// </summary>
465
                /// <param name="value">The value.</param>
466
                /// <param name="prefix">The prefix.</param>
467
                /// <returns>The value including the prefix.</returns>
468
                internal static string EnsureStartsWith(this string value, string prefix)
469
                {
470
                        return !string.IsNullOrEmpty(value) && value.StartsWith(prefix) ? value : string.Concat(prefix, value);
×
471
                }
472

473
                /// <summary>
474
                /// Ensure that a string ends with a given suffix.
475
                /// </summary>
476
                /// <param name="value">The value.</param>
477
                /// <param name="suffix">The sufix.</param>
478
                /// <returns>The value including the suffix.</returns>
479
                internal static string EnsureEndsWith(this string value, string suffix)
480
                {
481
                        return !string.IsNullOrEmpty(value) && value.EndsWith(suffix) ? value : string.Concat(value, suffix);
×
482
                }
483

484
                internal static JsonElement? GetProperty(this JsonElement element, string name, bool throwIfMissing = true)
485
                {
486
                        var parts = name.Split('/');
79✔
487
                        if (!element.TryGetProperty(parts[0], out var property))
79✔
488
                        {
489
                                if (throwIfMissing) throw new ArgumentException($"Unable to find '{name}'", nameof(name));
9✔
490
                                else return null;
9✔
491
                        }
492

493
                        foreach (var part in parts.Skip(1))
148✔
494
                        {
495
                                if (!property.TryGetProperty(part, out property))
4✔
496
                                {
497
                                        if (throwIfMissing) throw new ArgumentException($"Unable to find '{name}'", nameof(name));
×
498
                                        else return null;
×
499
                                }
500
                        }
501

502
                        return property;
70✔
503
                }
×
504

505
                internal static T GetPropertyValue<T>(this JsonElement element, string name, T defaultValue)
506
                {
507
                        return GetPropertyValue<T>(element, new[] { name }, defaultValue, false);
33✔
508
                }
509

510
                internal static T GetPropertyValue<T>(this JsonElement element, string[] names, T defaultValue)
511
                {
512
                        return GetPropertyValue<T>(element, names, defaultValue, false);
×
513
                }
514

515
                internal static T GetPropertyValue<T>(this JsonElement element, string name)
516
                {
517
                        return GetPropertyValue<T>(element, new[] { name }, default, true);
25✔
518
                }
519

520
                internal static T GetPropertyValue<T>(this JsonElement element, string[] names)
521
                {
522
                        return GetPropertyValue<T>(element, names, default, true);
×
523
                }
524

525
                internal static async Task<TResult[]> ForEachAsync<T, TResult>(this IEnumerable<T> items, Func<T, Task<TResult>> action, int maxDegreeOfParalellism)
526
                {
527
                        var allTasks = new List<Task<TResult>>();
528
                        using (var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism))
529
                        {
530
                                foreach (var item in items)
531
                                {
532
                                        await throttler.WaitAsync();
533
                                        allTasks.Add(
534
                                                Task.Run(async () =>
535
                                                {
536
                                                        try
537
                                                        {
538
                                                                return await action(item).ConfigureAwait(false);
539
                                                        }
540
                                                        finally
541
                                                        {
542
                                                                throttler.Release();
543
                                                        }
544
                                                }));
545
                                }
546

547
                                var results = await Task.WhenAll(allTasks).ConfigureAwait(false);
548
                                return results;
549
                        }
550
                }
551

552
                internal static async Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, Task> action, int maxDegreeOfParalellism)
553
                {
554
                        var allTasks = new List<Task>();
555
                        using (var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism))
556
                        {
557
                                foreach (var item in items)
558
                                {
559
                                        await throttler.WaitAsync();
560
                                        allTasks.Add(
561
                                                Task.Run(async () =>
562
                                                {
563
                                                        try
564
                                                        {
565
                                                                await action(item).ConfigureAwait(false);
566
                                                        }
567
                                                        finally
568
                                                        {
569
                                                                throttler.Release();
570
                                                        }
571
                                                }));
572
                                }
573

574
                                await Task.WhenAll(allTasks).ConfigureAwait(false);
575
                        }
576
                }
577

578
                /// <summary>
579
                /// Gets the attribute of the specified type.
580
                /// </summary>
581
                /// <typeparam name="T">The type of the desired attribute.</typeparam>
582
                /// <param name="enumVal">The enum value.</param>
583
                /// <returns>The attribute.</returns>
584
                internal static T GetAttributeOfType<T>(this Enum enumVal)
585
                        where T : Attribute
586
                {
587
                        return enumVal.GetType()
5✔
588
                                .GetTypeInfo()
5✔
589
                                .DeclaredMembers
5✔
590
                                .SingleOrDefault(x => x.Name == enumVal.ToString())
5✔
591
                                ?.GetCustomAttribute<T>(false);
5✔
592
                }
593

594
                /// <summary>
595
                /// Indicates if an object contain a numerical value.
596
                /// </summary>
597
                /// <param name="value">The object.</param>
598
                /// <returns>A boolean indicating if the object contains a numerical value.</returns>
599
                internal static bool IsNumber(this object value)
600
                {
601
                        return value is sbyte
×
602
                                   || value is byte
×
603
                                   || value is short
×
604
                                   || value is ushort
×
605
                                   || value is int
×
606
                                   || value is uint
×
607
                                   || value is long
×
608
                                   || value is ulong
×
609
                                   || value is float
×
610
                                   || value is double
×
611
                                   || value is decimal;
×
612
                }
613

614
                /// <summary>
615
                /// Returns the first value for a specified header stored in the System.Net.Http.Headers.HttpHeaderscollection.
616
                /// </summary>
617
                /// <param name="headers">The HTTP headers.</param>
618
                /// <param name="name">The specified header to return value for.</param>
619
                /// <returns>A string.</returns>
620
                internal static string GetValue(this HttpHeaders headers, string name)
621
                {
622
                        if (headers == null) return null;
6✔
623

624
                        if (headers.TryGetValues(name, out IEnumerable<string> values))
6✔
625
                        {
626
                                return values.FirstOrDefault();
6✔
627
                        }
628

629
                        return null;
×
630
                }
631

632
                internal static IEnumerable<KeyValuePair<string, string>> ParseQuerystring(this Uri uri)
633
                {
634
                        var querystringParameters = uri
×
635
                                .Query.TrimStart('?')
×
636
                                .Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries)
×
637
                                .Select(value => value.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries))
×
638
                                .Select(splitValue =>
×
639
                                {
×
640
                                        if (splitValue.Length == 1)
×
641
                                        {
×
642
                                                return new KeyValuePair<string, string>(splitValue[0].Trim(), null);
×
643
                                        }
×
644
                                        else
×
645
                                        {
×
646
                                                return new KeyValuePair<string, string>(splitValue[0].Trim(), splitValue[1].Trim());
×
647
                                        }
×
648
                                });
×
649

650
                        return querystringParameters;
×
651
                }
652

653
                internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response)
654
                {
655
                        var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME);
3✔
656
                        DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo);
3✔
657
                        return diagnosticInfo;
3✔
658
                }
659

660
                internal static async Task<(bool IsError, string ErrorMessage, int? ErrorCode)> GetErrorMessageAsync(this HttpResponseMessage message)
661
                {
662
                        // Default error code
663
                        int? errorCode = null;
664

665
                        // Default error message
666
                        var errorMessage = $"{(int)message.StatusCode}: {message.ReasonPhrase}";
667

668
                        /*
669
                                In case of an error, the Zoom API returns a JSON string that looks like this:
670
                                {
671
                                        "code": 300,
672
                                        "message": "This meeting has not registration required: 544993922"
673
                                }
674

675
                                Sometimes, the JSON string contains additional info like this example:
676
                                {
677
                                        "code":300,
678
                                        "message":"Validation Failed.",
679
                                        "errors":[
680
                                                {
681
                                                        "field":"settings.jbh_time",
682
                                                        "message":"Invalid parameter: jbh_time."
683
                                                }
684
                                        ]
685
                                }
686
                        */
687

688
                        var responseContent = await message.Content.ReadAsStringAsync(null).ConfigureAwait(false);
689

690
                        if (!string.IsNullOrEmpty(responseContent))
691
                        {
692
                                try
693
                                {
694
                                        var rootJsonElement = JsonDocument.Parse(responseContent).RootElement;
695

696
                                        if (rootJsonElement.ValueKind == JsonValueKind.Object)
697
                                        {
698
                                                errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null;
699
                                                errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage);
700
                                                if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails))
701
                                                {
702
                                                        var errorDetails = string.Join(
703
                                                                " ",
704
                                                                jsonErrorDetails
705
                                                                        .EnumerateArray()
706
                                                                        .Select(jsonErrorDetail =>
707
                                                {
708
                                                        var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty;
709
                                                        return errorDetail;
710
                                                })
711
                                                                        .Where(message => !string.IsNullOrEmpty(message)));
712

713
                                                        if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}";
714
                                                }
715

716
                                                return (errorCode.HasValue, errorMessage, errorCode);
717
                                        }
718
                                }
719
                                catch
720
                                {
721
                                        // Intentionally ignore parsing errors
722
                                }
723
                        }
724

725
                        return (!message.IsSuccessStatusCode, errorMessage, errorCode);
726
                }
727

728
                internal static async Task<Stream> CompressAsync(this Stream source)
729
                {
730
                        var compressedStream = new MemoryStream();
731
                        using (var gzip = new GZipStream(compressedStream, CompressionMode.Compress, true))
732
                        {
733
                                await source.CopyToAsync(gzip).ConfigureAwait(false);
734
                        }
735

736
                        compressedStream.Position = 0;
737
                        return compressedStream;
738
                }
739

740
                internal static async Task<Stream> DecompressAsync(this Stream source)
741
                {
742
                        var decompressedStream = new MemoryStream();
743
                        using (var gzip = new GZipStream(source, CompressionMode.Decompress, true))
744
                        {
745
                                await gzip.CopyToAsync(decompressedStream).ConfigureAwait(false);
746
                        }
747

748
                        decompressedStream.Position = 0;
749
                        return decompressedStream;
750
                }
751

752
                /// <summary>Convert an enum to its string representation.</summary>
753
                /// <typeparam name="T">The enum type.</typeparam>
754
                /// <param name="enumValue">The value.</param>
755
                /// <returns>The string representation of the enum value.</returns>
756
                /// <remarks>Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .</remarks>
757
                internal static string ToEnumString<T>(this T enumValue)
758
                        where T : Enum
759
                {
760
                        if (TryToEnumString(enumValue, out string stringValue)) return stringValue;
10✔
761
                        return enumValue.ToString();
×
762
                }
763

764
                internal static bool TryToEnumString<T>(this T enumValue, out string stringValue)
765
                        where T : Enum
766
                {
767
                        var enumMemberAttribute = enumValue.GetAttributeOfType<EnumMemberAttribute>();
5✔
768
                        if (enumMemberAttribute != null)
5✔
769
                        {
770
                                stringValue = enumMemberAttribute.Value;
5✔
771
                                return true;
5✔
772
                        }
773

774
                        var jsonPropertyNameAttribute = enumValue.GetAttributeOfType<JsonPropertyNameAttribute>();
×
775
                        if (jsonPropertyNameAttribute != null)
×
776
                        {
777
                                stringValue = jsonPropertyNameAttribute.Name;
×
778
                                return true;
×
779
                        }
780

781
                        var descriptionAttribute = enumValue.GetAttributeOfType<DescriptionAttribute>();
×
782
                        if (descriptionAttribute != null)
×
783
                        {
784
                                stringValue = descriptionAttribute.Description;
×
785
                                return true;
×
786
                        }
787

788
                        stringValue = null;
×
789
                        return false;
×
790
                }
791

792
                /// <summary>Parses a string into its corresponding enum value.</summary>
793
                /// <typeparam name="T">The enum type.</typeparam>
794
                /// <param name="str">The string value.</param>
795
                /// <returns>The enum representation of the string value.</returns>
796
                /// <remarks>Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .</remarks>
797
                internal static T ToEnum<T>(this string str)
798
                        where T : Enum
799
                {
800
                        if (TryToEnum(str, out T enumValue)) return enumValue;
276✔
801

802
                        throw new ArgumentException($"There is no value in the {typeof(T).Name} enum that corresponds to '{str}'.");
×
803
                }
804

805
                internal static bool TryToEnum<T>(this string str, out T enumValue)
806
                        where T : Enum
807
                {
808
                        var enumType = typeof(T);
138✔
809
                        foreach (var name in Enum.GetNames(enumType))
1,238✔
810
                        {
811
                                var customAttributes = enumType.GetField(name).GetCustomAttributes(true);
550✔
812

813
                                // See if there's a matching 'EnumMember' attribute
814
                                if (customAttributes.OfType<EnumMemberAttribute>().Any(attribute => string.Equals(attribute.Value, str, StringComparison.OrdinalIgnoreCase)))
550✔
815
                                {
816
                                        enumValue = (T)Enum.Parse(enumType, name);
138✔
817
                                        return true;
138✔
818
                                }
819

820
                                // See if there's a matching 'JsonPropertyName' attribute
821
                                if (customAttributes.OfType<JsonPropertyNameAttribute>().Any(attribute => string.Equals(attribute.Name, str, StringComparison.OrdinalIgnoreCase)))
412✔
822
                                {
823
                                        enumValue = (T)Enum.Parse(enumType, name);
×
824
                                        return true;
×
825
                                }
826

827
                                // See if there's a matching 'Description' attribute
828
                                if (customAttributes.OfType<DescriptionAttribute>().Any(attribute => string.Equals(attribute.Description, str, StringComparison.OrdinalIgnoreCase)))
412✔
829
                                {
830
                                        enumValue = (T)Enum.Parse(enumType, name);
×
831
                                        return true;
×
832
                                }
833

834
                                // See if the value matches the name
835
                                if (string.Equals(name, str, StringComparison.OrdinalIgnoreCase))
412✔
836
                                {
837
                                        enumValue = (T)Enum.Parse(enumType, name);
×
838
                                        return true;
×
839
                                }
840
                        }
841

842
                        enumValue = default;
×
843
                        return false;
×
844
                }
845

846
                internal static T ToObject<T>(this JsonElement element, JsonSerializerOptions options = null)
847
                {
848
                        return JsonSerializer.Deserialize<T>(element.GetRawText(), options ?? JsonFormatter.DeserializerOptions);
20✔
849
                }
850

851
                internal static void Add<T>(this JsonObject jsonObject, string propertyName, T value)
852
                {
853
                        if (value is IEnumerable<T> items)
×
854
                        {
855
                                var jsonArray = new JsonArray();
×
856
                                foreach (var item in items)
×
857
                                {
858
                                        jsonArray.Add(item);
×
859
                                }
860

861
                                jsonObject.Add(propertyName, jsonArray);
×
862
                        }
863
                        else
864
                        {
865
                                jsonObject.Add(propertyName, JsonValue.Create(value));
×
866
                        }
867
                }
×
868

869
                internal static string ToHexString(this byte[] bytes)
870
                {
871
                        var result = new StringBuilder(bytes.Length * 2);
×
872
                        for (int i = 0; i < bytes.Length; i++)
×
873
                                result.Append(bytes[i].ToString("x2"));
×
874
                        return result.ToString();
×
875
                }
876

877
                internal static string ToExactLength(this string source, int totalWidth, string postfix = "...", char paddingChar = ' ')
878
                {
879
                        if (string.IsNullOrEmpty(source)) return new string(paddingChar, totalWidth);
×
880
                        if (source.Length <= totalWidth) return source.PadRight(totalWidth, paddingChar);
×
881
                        var result = $"{source.Substring(0, totalWidth - (postfix?.Length ?? 0))}{postfix ?? string.Empty}";
×
882
                        return result;
×
883
                }
884

885
                /// <summary>Asynchronously converts the JSON encoded content and convert it to an object of the desired type.</summary>
886
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
887
                /// <param name="httpContent">The content.</param>
888
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
889
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
890
                /// <param name="options">Options to control behavior Converter during parsing.</param>
891
                /// <param name="cancellationToken">The cancellation token.</param>
892
                /// <returns>Returns the strongly typed object.</returns>
893
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
894
                private static async Task<T> AsObject<T>(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
895
                {
896
                        var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
897

898
                        if (string.IsNullOrEmpty(propertyName))
899
                        {
900
                                return JsonSerializer.Deserialize<T>(responseContent, options ?? JsonFormatter.DeserializerOptions);
901
                        }
902

903
                        var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default);
904
                        if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
905
                        {
906
                                return property.ToObject<T>(options);
907
                        }
908
                        else if (throwIfPropertyIsMissing)
909
                        {
910
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
911
                        }
912
                        else
913
                        {
914
                                return default;
915
                        }
916
                }
917

918
                /// <summary>Get a raw JSON object representation of the response.</summary>
919
                /// <param name="httpContent">The content.</param>
920
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
921
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
922
                /// <param name="cancellationToken">The cancellation token.</param>
923
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
924
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
925
                private static async Task<JsonDocument> AsRawJsonDocument(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, CancellationToken cancellationToken = default)
926
                {
927
                        var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
928

929
                        var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default);
930

931
                        if (string.IsNullOrEmpty(propertyName))
932
                        {
933
                                return jsonDoc;
934
                        }
935

936
                        if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
937
                        {
938
                                var propertyContent = property.GetRawText();
939
                                return JsonDocument.Parse(propertyContent, (JsonDocumentOptions)default);
940
                        }
941
                        else if (throwIfPropertyIsMissing)
942
                        {
943
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
944
                        }
945
                        else
946
                        {
947
                                return default;
948
                        }
949
                }
950

951
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponse' object.</summary>
952
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
953
                /// <param name="httpContent">The content.</param>
954
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
955
                /// <param name="options">Options to control behavior Converter during parsing.</param>
956
                /// <param name="cancellationToken">The cancellation token.</param>
957
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
958
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
959
                private static async Task<PaginatedResponse<T>> AsPaginatedResponse<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
960
                {
961
                        // Get the content as a queryable json document
962
                        var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false);
963
                        var rootElement = doc.RootElement;
964

965
                        // Get the various metadata properties
966
                        var pageCount = rootElement.GetPropertyValue("page_count", 0);
967
                        var pageNumber = rootElement.GetPropertyValue("page_number", 0);
968
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
969
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
970

971
                        // Get the property that holds the records
972
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
973

974
                        // Make sure the desired property is present. It's ok if the property is missing when there are no records.
975
                        if (!jsonProperty.HasValue && totalRecords is > 0)
976
                        {
977
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
978
                        }
979

980
                        var result = new PaginatedResponse<T>()
981
                        {
982
                                PageCount = pageCount,
983
                                PageNumber = pageNumber,
984
                                PageSize = pageSize,
985
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
986
                        };
987
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
988

989
                        return result;
990
                }
991

992
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithToken' object.</summary>
993
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
994
                /// <param name="httpContent">The content.</param>
995
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
996
                /// <param name="options">Options to control behavior Converter during parsing.</param>
997
                /// <param name="cancellationToken">The cancellation token.</param>
998
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
999
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1000
                private static async Task<PaginatedResponseWithToken<T>> AsPaginatedResponseWithToken<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1001
                {
1002
                        // Get the content as a queryable json document
1003
                        var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false);
1004
                        var rootElement = doc.RootElement;
1005

1006
                        // Get the various metadata properties
1007
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1008
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1009
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1010

1011
                        // Get the property that holds the records
1012
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1013

1014
                        // Make sure the desired property is present. It's ok if the property is missing when there are no records.
1015
                        if (!jsonProperty.HasValue && totalRecords is > 0)
1016
                        {
1017
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
1018
                        }
1019

1020
                        var result = new PaginatedResponseWithToken<T>()
1021
                        {
1022
                                NextPageToken = nextPageToken,
1023
                                PageSize = pageSize,
1024
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1025
                        };
1026
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1027

1028
                        return result;
1029
                }
1030

1031
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithToken' object.</summary>
1032
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
1033
                /// <param name="httpContent">The content.</param>
1034
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1035
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1036
                /// <param name="cancellationToken">The cancellation token.</param>
1037
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
1038
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1039
                private static async Task<PaginatedResponseWithTokenAndDateRange<T>> AsPaginatedResponseWithTokenAndDateRange<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1040
                {
1041
                        // Get the content as a queryable json document
1042
                        var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false);
1043
                        var rootElement = doc.RootElement;
1044

1045
                        // Get the various metadata properties
1046
                        var from = DateTime.ParseExact(rootElement.GetPropertyValue("from", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1047
                        var to = DateTime.ParseExact(rootElement.GetPropertyValue("to", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1048
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1049
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1050
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1051

1052
                        // Get the property that holds the records
1053
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1054

1055
                        // Make sure the desired property is present. It's ok if the property is missing when there are no records.
1056
                        if (!jsonProperty.HasValue && totalRecords is > 0)
1057
                        {
1058
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
1059
                        }
1060

1061
                        var result = new PaginatedResponseWithTokenAndDateRange<T>()
1062
                        {
1063
                                From = from,
1064
                                To = to,
1065
                                NextPageToken = nextPageToken,
1066
                                PageSize = pageSize,
1067
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1068
                        };
1069
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1070

1071
                        return result;
1072
                }
1073

1074
                private static T GetPropertyValue<T>(this JsonElement element, string[] names, T defaultValue, bool throwIfMissing)
1075
                {
1076
                        JsonElement? property = null;
58✔
1077

1078
                        foreach (var name in names)
177✔
1079
                        {
1080
                                property = element.GetProperty(name, false);
58✔
1081
                                if (property.HasValue) break;
58✔
1082
                        }
1083

1084
                        if (!property.HasValue) return defaultValue;
61✔
1085

1086
                        var typeOfT = typeof(T);
55✔
1087

1088
                        if (typeOfT.IsEnum)
55✔
1089
                        {
1090
                                return property.Value.ValueKind switch
7✔
1091
                                {
7✔
1092
                                        JsonValueKind.String => (T)Enum.Parse(typeof(T), property.Value.GetString()),
×
1093
                                        JsonValueKind.Number => (T)Enum.ToObject(typeof(T), property.Value.GetInt16()),
7✔
1094
                                        _ => throw new ArgumentException($"Unable to convert a {property.Value.ValueKind} into a {typeof(T).FullName}", nameof(T)),
×
1095
                                };
7✔
1096
                        }
1097

1098
                        if (typeOfT.IsGenericType && typeOfT.GetGenericTypeDefinition() == typeof(Nullable<>))
48✔
1099
                        {
1100
                                var underlyingType = Nullable.GetUnderlyingType(typeOfT);
7✔
1101
                                var getElementValue = typeof(Internal)
7✔
1102
                                        .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
7✔
1103
                                        .MakeGenericMethod(underlyingType);
7✔
1104

1105
                                return (T)getElementValue.Invoke(null, new object[] { property.Value });
7✔
1106
                        }
1107

1108
                        if (typeOfT.IsArray)
41✔
1109
                        {
1110
                                var elementType = typeOfT.GetElementType();
1✔
1111
                                var getElementValue = typeof(Internal)
1✔
1112
                                        .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
1✔
1113
                                        .MakeGenericMethod(elementType);
1✔
1114

1115
                                var arrayList = new ArrayList(property.Value.GetArrayLength());
1✔
1116
                                foreach (var arrayElement in property.Value.EnumerateArray())
4✔
1117
                                {
1118
                                        var elementValue = getElementValue.Invoke(null, new object[] { arrayElement });
1✔
1119
                                        arrayList.Add(elementValue);
1✔
1120
                                }
1121

1122
                                return (T)Convert.ChangeType(arrayList.ToArray(elementType), typeof(T));
1✔
1123
                        }
1124

1125
                        return property.Value.GetElementValue<T>();
40✔
1126
                }
1127

1128
                private static T GetElementValue<T>(this JsonElement element)
1129
                {
1130
                        var typeOfT = typeof(T);
48✔
1131

1132
                        return typeOfT switch
48✔
1133
                        {
48✔
1134
                                Type boolType when boolType == typeof(bool) => (T)(object)element.GetBoolean(),
48✔
1135
                                Type strType when strType == typeof(string) => (T)(object)element.GetString(),
72✔
1136
                                Type bytesType when bytesType == typeof(byte[]) => (T)(object)element.GetBytesFromBase64(),
24✔
1137
                                Type sbyteType when sbyteType == typeof(sbyte) => (T)(object)element.GetSByte(),
24✔
1138
                                Type byteType when byteType == typeof(byte) => (T)(object)element.GetByte(),
24✔
1139
                                Type shortType when shortType == typeof(short) => (T)(object)element.GetInt16(),
24✔
1140
                                Type ushortType when ushortType == typeof(ushort) => (T)(object)element.GetUInt16(),
24✔
1141
                                Type intType when intType == typeof(int) => (T)(object)element.GetInt32(),
39✔
1142
                                Type uintType when uintType == typeof(uint) => (T)(object)element.GetUInt32(),
9✔
1143
                                Type longType when longType == typeof(long) => (T)(object)element.GetInt64(),
18✔
1144
                                Type ulongType when ulongType == typeof(ulong) => (T)(object)element.GetUInt64(),
×
1145
                                Type doubleType when doubleType == typeof(double) => (T)(object)element.GetDouble(),
×
1146
                                Type floatType when floatType == typeof(float) => (T)(object)element.GetSingle(),
×
1147
                                Type decimalType when decimalType == typeof(decimal) => (T)(object)element.GetDecimal(),
×
1148
                                Type datetimeType when datetimeType == typeof(DateTime) => (T)(object)element.GetDateTime(),
×
1149
                                Type offsetType when offsetType == typeof(DateTimeOffset) => (T)(object)element.GetDateTimeOffset(),
×
1150
                                Type guidType when guidType == typeof(Guid) => (T)(object)element.GetGuid(),
×
1151
                                _ => throw new ArgumentException($"Unsable to map {typeof(T).FullName} to a corresponding JSON type", nameof(T)),
×
1152
                        };
48✔
1153
                }
1154
        }
1155
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc