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

Jericho / ZoomNet / 704

05 Apr 2024 11:26PM UTC coverage: 20.198% (+0.7%) from 19.504%
704

push

appveyor

Jericho
Merge branch 'release/0.75.0'

613 of 3035 relevant lines covered (20.2%)

12.2 hits per line

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

58.18
/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);
20✔
53
                        if (precision == UnixTimePrecision.Milliseconds) return EPOCH.AddMilliseconds(unixTime);
31✔
54
                        throw new ArgumentException($"Unknown precision: {precision}");
1✔
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;
7✔
69
                        if (precision == UnixTimePrecision.Seconds) return Convert.ToInt64(diff.TotalSeconds);
10✔
70
                        if (precision == UnixTimePrecision.Milliseconds) return Convert.ToInt64(diff.TotalMilliseconds);
7✔
71
                        throw new ArgumentException($"Unknown precision: {precision}");
1✔
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;
4✔
86
                        return date.Value.ToZoomFormat(timeZone, dateOnly);
2✔
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)
8✔
105
                        {
106
                                if (timeZone.HasValue && timeZone.Value == TimeZones.UTC) return date.ToUniversalTime().ToString(dateOnlyFormat);
7✔
107
                                else return date.ToString(dateOnlyFormat);
5✔
108
                        }
109
                        else
110
                        {
111
                                if (timeZone.HasValue && timeZone.Value == TimeZones.UTC) return date.ToUniversalTime().ToString(utcDateFormat);
3✔
112
                                else return date.ToString(defaultDateFormat);
1✔
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
                                const int DefaultBufferSize = 81920;
176
                                await contentStream.CopyToAsync(ms, DefaultBufferSize, cancellationToken).ConfigureAwait(false);
177
                                ms.Position = 0;
178
                                using var sr = new StreamReader(ms, encoding);
179
#if NET7_0_OR_GREATER
180
                                content = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
181
#else
182
                                content = await sr.ReadToEndAsync().ConfigureAwait(false);
183
#endif
184

185
                                // It's important to rewind the stream
186
                                if (contentStream.CanSeek) contentStream.Position = 0;
187
                        }
188

189
                        return content;
190
                }
191

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

232
                        return encoding;
26✔
233
                }
234

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

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

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

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

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

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

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

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

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

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

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

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

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

×
396
                                if (omitCharSet && !string.IsNullOrEmpty(httpContent.Headers.ContentType.CharSet))
×
397
                                {
×
398
                                        httpContent.Headers.ContentType.CharSet = string.Empty;
×
399
                                }
×
400

×
401
                                return httpContent;
×
402
                        });
×
403
                }
404

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

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

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

446
                        // In case the TimeSpan is extremely short
447
                        if (timeSpan.TotalMilliseconds <= 1) return "1 millisecond";
×
448

449
                        var result = new StringBuilder();
×
450
                        AppendFormatIfNecessary(result, "day", timeSpan.Days);
×
451
                        AppendFormatIfNecessary(result, "hour", timeSpan.Hours);
×
452
                        AppendFormatIfNecessary(result, "minute", timeSpan.Minutes);
×
453
                        AppendFormatIfNecessary(result, "second", timeSpan.Seconds);
×
454
                        AppendFormatIfNecessary(result, "millisecond", timeSpan.Milliseconds);
×
455
                        return result.ToString().Trim();
×
456
                }
457

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

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

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

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

498
                        return property;
76✔
499
                }
×
500

501
                internal static T GetPropertyValue<T>(this JsonElement element, string name, T defaultValue)
502
                {
503
                        return GetPropertyValue<T>(element, new[] { name }, defaultValue, false);
41✔
504
                }
505

506
                internal static T GetPropertyValue<T>(this JsonElement element, string[] names, T defaultValue)
507
                {
508
                        return GetPropertyValue<T>(element, names, defaultValue, false);
×
509
                }
510

511
                internal static T GetPropertyValue<T>(this JsonElement element, string name)
512
                {
513
                        return GetPropertyValue<T>(element, new[] { name }, default, true);
25✔
514
                }
515

516
                internal static T GetPropertyValue<T>(this JsonElement element, string[] names)
517
                {
518
                        return GetPropertyValue<T>(element, names, default, true);
×
519
                }
520

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

542
                        var results = await Task.WhenAll(allTasks).ConfigureAwait(false);
543
                        return results;
544
                }
545

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

567
                        await Task.WhenAll(allTasks).ConfigureAwait(false);
568
                }
569

570
                /// <summary>
571
                /// Gets the attribute of the specified type.
572
                /// </summary>
573
                /// <typeparam name="T">The type of the desired attribute.</typeparam>
574
                /// <param name="enumVal">The enum value.</param>
575
                /// <returns>The attribute.</returns>
576
                internal static T GetAttributeOfType<T>(this Enum enumVal)
577
                        where T : Attribute
578
                {
579
                        return enumVal.GetType()
27✔
580
                                .GetTypeInfo()
27✔
581
                                .DeclaredMembers
27✔
582
                                .SingleOrDefault(x => x.Name == enumVal.ToString())
27✔
583
                                ?.GetCustomAttribute<T>(false);
27✔
584
                }
585

586
                /// <summary>
587
                /// Indicates if an object contain a numerical value.
588
                /// </summary>
589
                /// <param name="value">The object.</param>
590
                /// <returns>A boolean indicating if the object contains a numerical value.</returns>
591
                internal static bool IsNumber(this object value)
592
                {
593
                        return value is sbyte
×
594
                                   || value is byte
×
595
                                   || value is short
×
596
                                   || value is ushort
×
597
                                   || value is int
×
598
                                   || value is uint
×
599
                                   || value is long
×
600
                                   || value is ulong
×
601
                                   || value is float
×
602
                                   || value is double
×
603
                                   || value is decimal;
×
604
                }
605

606
                /// <summary>
607
                /// Returns the first value for a specified header stored in the System.Net.Http.Headers.HttpHeaderscollection.
608
                /// </summary>
609
                /// <param name="headers">The HTTP headers.</param>
610
                /// <param name="name">The specified header to return value for.</param>
611
                /// <returns>A string.</returns>
612
                internal static string GetValue(this HttpHeaders headers, string name)
613
                {
614
                        if (headers == null) return null;
12✔
615

616
                        if (headers.TryGetValues(name, out IEnumerable<string> values))
12✔
617
                        {
618
                                return values.FirstOrDefault();
12✔
619
                        }
620

621
                        return null;
×
622
                }
623

624
                internal static IEnumerable<KeyValuePair<string, string>> ParseQuerystring(this Uri uri)
625
                {
626
                        var querystringParameters = uri
×
627
                                .Query.TrimStart('?')
×
628
                                .Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries)
×
629
                                .Select(value => value.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries))
×
630
                                .Select(splitValue =>
×
631
                                {
×
632
                                        if (splitValue.Length == 1)
×
633
                                        {
×
634
                                                return new KeyValuePair<string, string>(splitValue[0].Trim(), null);
×
635
                                        }
×
636
                                        else
×
637
                                        {
×
638
                                                return new KeyValuePair<string, string>(splitValue[0].Trim(), splitValue[1].Trim());
×
639
                                        }
×
640
                                });
×
641

642
                        return querystringParameters;
×
643
                }
644

645
                internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response)
646
                {
647
                        var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME);
6✔
648
                        DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo);
6✔
649
                        return diagnosticInfo;
6✔
650
                }
651

652
                internal static async Task<(bool IsError, string ErrorMessage, int? ErrorCode)> GetErrorMessageAsync(this HttpResponseMessage message)
653
                {
654
                        // Default error code
655
                        int? errorCode = null;
656

657
                        // Default error message
658
                        var errorMessage = $"{(int)message.StatusCode}: {message.ReasonPhrase}";
659

660
                        /*
661
                                In case of an error, the Zoom API returns a JSON string that looks like this:
662
                                {
663
                                        "code": 300,
664
                                        "message": "This meeting has not registration required: 544993922"
665
                                }
666

667
                                Sometimes, the JSON string contains additional info like this example:
668
                                {
669
                                        "code":300,
670
                                        "message":"Validation Failed.",
671
                                        "errors":[
672
                                                {
673
                                                        "field":"settings.jbh_time",
674
                                                        "message":"Invalid parameter: jbh_time."
675
                                                }
676
                                        ]
677
                                }
678
                        */
679

680
                        var responseContent = await message.Content.ReadAsStringAsync(null).ConfigureAwait(false);
681

682
                        if (!string.IsNullOrEmpty(responseContent))
683
                        {
684
                                try
685
                                {
686
                                        var rootJsonElement = JsonDocument.Parse(responseContent).RootElement;
687

688
                                        if (rootJsonElement.ValueKind == JsonValueKind.Object)
689
                                        {
690
                                                errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? (int?)jsonErrorCode.GetInt32() : (int?)null;
691
                                                errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : (errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage);
692
                                                if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails))
693
                                                {
694
                                                        var errorDetails = string.Join(
695
                                                                " ",
696
                                                                jsonErrorDetails
697
                                                                        .EnumerateArray()
698
                                                                        .Select(jsonErrorDetail =>
699
                                                {
700
                                                        var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty;
701
                                                        return errorDetail;
702
                                                })
703
                                                                        .Where(message => !string.IsNullOrEmpty(message)));
704

705
                                                        if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}";
706
                                                }
707

708
                                                return (errorCode.HasValue, errorMessage, errorCode);
709
                                        }
710
                                }
711
                                catch
712
                                {
713
                                        // Intentionally ignore parsing errors
714
                                }
715
                        }
716

717
                        return (!message.IsSuccessStatusCode, errorMessage, errorCode);
718
                }
719

720
                internal static async Task<Stream> CompressAsync(this Stream source)
721
                {
722
                        var compressedStream = new MemoryStream();
723
                        using (var gzip = new GZipStream(compressedStream, CompressionMode.Compress, true))
724
                        {
725
                                await source.CopyToAsync(gzip).ConfigureAwait(false);
726
                        }
727

728
                        compressedStream.Position = 0;
729
                        return compressedStream;
730
                }
731

732
                internal static async Task<Stream> DecompressAsync(this Stream source)
733
                {
734
                        var decompressedStream = new MemoryStream();
735
                        using (var gzip = new GZipStream(source, CompressionMode.Decompress, true))
736
                        {
737
                                await gzip.CopyToAsync(decompressedStream).ConfigureAwait(false);
738
                        }
739

740
                        decompressedStream.Position = 0;
741
                        return decompressedStream;
742
                }
743

744
                /// <summary>Convert an enum to its string representation.</summary>
745
                /// <typeparam name="T">The enum type.</typeparam>
746
                /// <param name="enumValue">The value.</param>
747
                /// <returns>The string representation of the enum value.</returns>
748
                /// <remarks>Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .</remarks>
749
                internal static string ToEnumString<T>(this T enumValue)
750
                        where T : Enum
751
                {
752
                        if (TryToEnumString(enumValue, out string stringValue)) return stringValue;
23✔
753
                        return enumValue.ToString();
1✔
754
                }
755

756
                internal static bool TryToEnumString<T>(this T enumValue, out string stringValue)
757
                        where T : Enum
758
                {
759
                        var multipleValuesEnumMemberAttribute = enumValue.GetAttributeOfType<MultipleValuesEnumMemberAttribute>();
12✔
760
                        if (multipleValuesEnumMemberAttribute != null)
12✔
761
                        {
762
                                stringValue = multipleValuesEnumMemberAttribute.DefaultValue;
2✔
763
                                return true;
2✔
764
                        }
765

766
                        var enumMemberAttribute = enumValue.GetAttributeOfType<EnumMemberAttribute>();
10✔
767
                        if (enumMemberAttribute != null)
10✔
768
                        {
769
                                stringValue = enumMemberAttribute.Value;
7✔
770
                                return true;
7✔
771
                        }
772

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

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

787
                        stringValue = null;
1✔
788
                        return false;
1✔
789
                }
790

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

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

804
                internal static bool TryToEnum<T>(this string str, out T enumValue)
805
                        where T : Enum
806
                {
807
                        var enumType = typeof(T);
196✔
808
                        foreach (var name in Enum.GetNames(enumType))
7,008✔
809
                        {
810
                                var customAttributes = enumType.GetField(name).GetCustomAttributes(true);
3,406✔
811

812
                                // See if there's a matching 'MultipleValuesEnumMember' attribute
813
                                if (customAttributes.OfType<MultipleValuesEnumMemberAttribute>().Any(attribute => string.Equals(attribute.DefaultValue, str, StringComparison.OrdinalIgnoreCase) ||
3,406✔
814
                                        (attribute.OtherValues ?? Array.Empty<string>()).Any(otherValue => string.Equals(otherValue, str, StringComparison.OrdinalIgnoreCase))))
3,406✔
815
                                {
816
                                        enumValue = (T)Enum.Parse(enumType, name);
5✔
817
                                        return true;
5✔
818
                                }
819

820
                                // See if there's a matching 'EnumMember' attribute
821
                                if (customAttributes.OfType<EnumMemberAttribute>().Any(attribute => string.Equals(attribute.Value, str, StringComparison.OrdinalIgnoreCase)))
3,401✔
822
                                {
823
                                        enumValue = (T)Enum.Parse(enumType, name);
188✔
824
                                        return true;
188✔
825
                                }
826

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

834
                                // See if there's a matching 'Description' attribute
835
                                if (customAttributes.OfType<DescriptionAttribute>().Any(attribute => string.Equals(attribute.Description, str, StringComparison.OrdinalIgnoreCase)))
3,212✔
836
                                {
837
                                        enumValue = (T)Enum.Parse(enumType, name);
1✔
838
                                        return true;
1✔
839
                                }
840

841
                                // See if the value matches the name
842
                                if (string.Equals(name, str, StringComparison.OrdinalIgnoreCase))
3,211✔
843
                                {
844
                                        enumValue = (T)Enum.Parse(enumType, name);
1✔
845
                                        return true;
1✔
846
                                }
847
                        }
848

849
                        enumValue = default;
×
850
                        return false;
×
851
                }
852

853
                internal static T ToObject<T>(this JsonElement element, JsonSerializerOptions options = null)
854
                {
855
                        return JsonSerializer.Deserialize<T>(element.GetRawText(), options ?? JsonFormatter.DeserializerOptions);
21✔
856
                }
857

858
                internal static void Add<T>(this JsonObject jsonObject, string propertyName, T value)
859
                {
860
                        if (value is IEnumerable<T> items)
×
861
                        {
862
                                var jsonArray = new JsonArray();
×
863
                                foreach (var item in items)
×
864
                                {
865
                                        jsonArray.Add(item);
×
866
                                }
867

868
                                jsonObject.Add(propertyName, jsonArray);
×
869
                        }
870
                        else
871
                        {
872
                                jsonObject.Add(propertyName, JsonValue.Create(value));
×
873
                        }
874
                }
×
875

876
                internal static string ToHexString(this byte[] bytes)
877
                {
878
                        var result = new StringBuilder(bytes.Length * 2);
×
879
                        for (int i = 0; i < bytes.Length; i++)
×
880
                                result.Append(bytes[i].ToString("x2"));
×
881
                        return result.ToString();
×
882
                }
883

884
                internal static string ToExactLength(this string source, int totalWidth, string postfix = "...", char paddingChar = ' ')
885
                {
886
                        if (string.IsNullOrEmpty(source)) return new string(paddingChar, totalWidth);
×
887
                        if (source.Length <= totalWidth) return source.PadRight(totalWidth, paddingChar);
×
888
                        var result = $"{source.Substring(0, totalWidth - (postfix?.Length ?? 0))}{postfix ?? string.Empty}";
×
889
                        return result;
×
890
                }
891

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

905
                        if (string.IsNullOrEmpty(propertyName))
906
                        {
907
                                return JsonSerializer.Deserialize<T>(responseContent, options ?? JsonFormatter.DeserializerOptions);
908
                        }
909

910
                        var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default);
911
                        if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
912
                        {
913
                                return property.ToObject<T>(options);
914
                        }
915
                        else if (throwIfPropertyIsMissing)
916
                        {
917
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
918
                        }
919
                        else
920
                        {
921
                                return default;
922
                        }
923
                }
924

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

936
                        var jsonDoc = JsonDocument.Parse(responseContent, (JsonDocumentOptions)default);
937

938
                        if (string.IsNullOrEmpty(propertyName))
939
                        {
940
                                return jsonDoc;
941
                        }
942

943
                        if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
944
                        {
945
                                var propertyContent = property.GetRawText();
946
                                return JsonDocument.Parse(propertyContent, (JsonDocumentOptions)default);
947
                        }
948
                        else if (throwIfPropertyIsMissing)
949
                        {
950
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
951
                        }
952
                        else
953
                        {
954
                                return default;
955
                        }
956
                }
957

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

972
                        // Get the various metadata properties
973
                        var pageCount = rootElement.GetPropertyValue("page_count", 0);
974
                        var pageNumber = rootElement.GetPropertyValue("page_number", 0);
975
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
976
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
977

978
                        // Get the property that holds the records
979
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
980

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

987
                        var result = new PaginatedResponse<T>()
988
                        {
989
                                PageCount = pageCount,
990
                                PageNumber = pageNumber,
991
                                PageSize = pageSize,
992
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
993
                        };
994
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
995

996
                        return result;
997
                }
998

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

1013
                        // Get the various metadata properties
1014
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1015
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1016
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1017

1018
                        // Get the property that holds the records
1019
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1020

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

1027
                        var result = new PaginatedResponseWithToken<T>()
1028
                        {
1029
                                NextPageToken = nextPageToken,
1030
                                PageSize = pageSize,
1031
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1032
                        };
1033
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1034

1035
                        return result;
1036
                }
1037

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

1052
                        // Get the various metadata properties
1053
                        var from = DateTime.ParseExact(rootElement.GetPropertyValue("from", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1054
                        var to = DateTime.ParseExact(rootElement.GetPropertyValue("to", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1055
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1056
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1057
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1058

1059
                        // Get the property that holds the records
1060
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1061

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

1068
                        var result = new PaginatedResponseWithTokenAndDateRange<T>()
1069
                        {
1070
                                From = from,
1071
                                To = to,
1072
                                NextPageToken = nextPageToken,
1073
                                PageSize = pageSize,
1074
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1075
                        };
1076
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1077

1078
                        return result;
1079
                }
1080

1081
                private static T GetPropertyValue<T>(this JsonElement element, string[] names, T defaultValue, bool throwIfMissing)
1082
                {
1083
                        JsonElement? property = null;
66✔
1084

1085
                        foreach (var name in names)
204✔
1086
                        {
1087
                                property = element.GetProperty(name, false);
66✔
1088
                                if (property.HasValue) break;
66✔
1089
                        }
1090

1091
                        if (!property.HasValue) return defaultValue;
72✔
1092

1093
                        var typeOfT = typeof(T);
60✔
1094

1095
                        if (typeOfT.IsEnum)
60✔
1096
                        {
1097
                                return property.Value.ValueKind switch
7✔
1098
                                {
7✔
1099
                                        JsonValueKind.String => (T)Enum.Parse(typeof(T), property.Value.GetString()),
×
1100
                                        JsonValueKind.Number => (T)Enum.ToObject(typeof(T), property.Value.GetInt16()),
7✔
1101
                                        _ => throw new ArgumentException($"Unable to convert a {property.Value.ValueKind} into a {typeof(T).FullName}", nameof(T)),
×
1102
                                };
7✔
1103
                        }
1104

1105
                        if (typeOfT.IsGenericType && typeOfT.GetGenericTypeDefinition() == typeof(Nullable<>))
53✔
1106
                        {
1107
                                var underlyingType = Nullable.GetUnderlyingType(typeOfT);
8✔
1108
                                var getElementValue = typeof(Internal)
8✔
1109
                                        .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
8✔
1110
                                        .MakeGenericMethod(underlyingType);
8✔
1111

1112
                                return (T)getElementValue.Invoke(null, new object[] { property.Value });
8✔
1113
                        }
1114

1115
                        if (typeOfT.IsArray)
45✔
1116
                        {
1117
                                var elementType = typeOfT.GetElementType();
1✔
1118
                                var getElementValue = typeof(Internal)
1✔
1119
                                        .GetMethod(nameof(Internal.GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
1✔
1120
                                        .MakeGenericMethod(elementType);
1✔
1121

1122
                                var arrayList = new ArrayList(property.Value.GetArrayLength());
1✔
1123
                                foreach (var arrayElement in property.Value.EnumerateArray())
4✔
1124
                                {
1125
                                        var elementValue = getElementValue.Invoke(null, new object[] { arrayElement });
1✔
1126
                                        arrayList.Add(elementValue);
1✔
1127
                                }
1128

1129
                                return (T)Convert.ChangeType(arrayList.ToArray(elementType), typeof(T));
1✔
1130
                        }
1131

1132
                        return property.Value.GetElementValue<T>();
44✔
1133
                }
1134

1135
                private static T GetElementValue<T>(this JsonElement element)
1136
                {
1137
                        var typeOfT = typeof(T);
53✔
1138

1139
                        return typeOfT switch
53✔
1140
                        {
53✔
1141
                                Type boolType when boolType == typeof(bool) => (T)(object)element.GetBoolean(),
53✔
1142
                                Type strType when strType == typeof(string) => (T)(object)element.GetString(),
80✔
1143
                                Type bytesType when bytesType == typeof(byte[]) => (T)(object)element.GetBytesFromBase64(),
26✔
1144
                                Type sbyteType when sbyteType == typeof(sbyte) => (T)(object)element.GetSByte(),
26✔
1145
                                Type byteType when byteType == typeof(byte) => (T)(object)element.GetByte(),
26✔
1146
                                Type shortType when shortType == typeof(short) => (T)(object)element.GetInt16(),
26✔
1147
                                Type ushortType when ushortType == typeof(ushort) => (T)(object)element.GetUInt16(),
26✔
1148
                                Type intType when intType == typeof(int) => (T)(object)element.GetInt32(),
43✔
1149
                                Type uintType when uintType == typeof(uint) => (T)(object)element.GetUInt32(),
9✔
1150
                                Type longType when longType == typeof(long) => (T)(object)element.GetInt64(),
18✔
1151
                                Type ulongType when ulongType == typeof(ulong) => (T)(object)element.GetUInt64(),
×
1152
                                Type doubleType when doubleType == typeof(double) => (T)(object)element.GetDouble(),
×
1153
                                Type floatType when floatType == typeof(float) => (T)(object)element.GetSingle(),
×
1154
                                Type decimalType when decimalType == typeof(decimal) => (T)(object)element.GetDecimal(),
×
1155
                                Type datetimeType when datetimeType == typeof(DateTime) => (T)(object)element.GetDateTime(),
×
1156
                                Type offsetType when offsetType == typeof(DateTimeOffset) => (T)(object)element.GetDateTimeOffset(),
×
1157
                                Type guidType when guidType == typeof(Guid) => (T)(object)element.GetGuid(),
×
1158
                                _ => throw new ArgumentException($"Unsable to map {typeof(T).FullName} to a corresponding JSON type", nameof(T)),
×
1159
                        };
53✔
1160
                }
1161
        }
1162
}
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