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

Jericho / ZoomNet / 765

07 Aug 2024 03:41PM UTC coverage: 20.254% (+0.6%) from 19.667%
765

push

appveyor

Jericho
Merge branch 'release/0.80.0'

637 of 3145 relevant lines covered (20.25%)

11.96 hits per line

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

57.76
/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

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

38
                private static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
1✔
39
                private static readonly int DEFAULT_DEGREE_OF_PARALLELISM = Environment.ProcessorCount > 1 ? Environment.ProcessorCount / 2 : 1;
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;
24✔
219
                        try
220
                        {
221
                                var charset = content?.Headers?.ContentType?.CharSet;
24✔
222
                                if (!string.IsNullOrEmpty(charset))
24✔
223
                                {
224
                                        encoding = Encoding.GetEncoding(charset);
24✔
225
                                }
226
                        }
24✔
227
                        catch
×
228
                        {
229
                                encoding = defaultEncoding;
×
230
                        }
×
231

232
                        return encoding;
24✔
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);
4✔
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('/');
98✔
483
                        if (!element.TryGetProperty(parts[0], out var property))
98✔
484
                        {
485
                                if (throwIfMissing) throw new ArgumentException($"Unable to find '{name}'", nameof(name));
15✔
486
                                else return null;
15✔
487
                        }
488

489
                        foreach (var part in parts.Skip(1))
174✔
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;
83✔
499
                }
×
500

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

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

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

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

521
                internal static Task<TResult[]> ForEachAsync<T, TResult>(this IEnumerable<T> items, Func<T, Task<TResult>> action) => ForEachAsync(items, action, DEFAULT_DEGREE_OF_PARALLELISM);
×
522

523
                internal static Task<TResult[]> ForEachAsync<T, TResult>(this IEnumerable<T> items, Func<T, int, Task<TResult>> action) => ForEachAsync(items, action, DEFAULT_DEGREE_OF_PARALLELISM);
×
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
                        foreach (var item in items)
530
                        {
531
                                await throttler.WaitAsync();
532
                                allTasks.Add(
533
                                        Task.Run(async () =>
534
                                        {
535
                                                try
536
                                                {
537
                                                        return await action(item).ConfigureAwait(false);
538
                                                }
539
                                                finally
540
                                                {
541
                                                        throttler.Release();
542
                                                }
543
                                        }));
544
                        }
545

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

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

571
                        var results = await Task.WhenAll(allTasks).ConfigureAwait(false);
572
                        return results;
573
                }
574

575
                internal static Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, Task> action) => ForEachAsync(items, action, DEFAULT_DEGREE_OF_PARALLELISM);
×
576

577
                internal static Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, int, Task> action) => ForEachAsync(items, action, DEFAULT_DEGREE_OF_PARALLELISM);
×
578

579
                internal static async Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, Task> action, int maxDegreeOfParalellism)
580
                {
581
                        var allTasks = new List<Task>();
582
                        using var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism);
583
                        foreach (var item in items)
584
                        {
585
                                await throttler.WaitAsync();
586
                                allTasks.Add(
587
                                        Task.Run(async () =>
588
                                        {
589
                                                try
590
                                                {
591
                                                        await action(item).ConfigureAwait(false);
592
                                                }
593
                                                finally
594
                                                {
595
                                                        throttler.Release();
596
                                                }
597
                                        }));
598
                        }
599

600
                        await Task.WhenAll(allTasks).ConfigureAwait(false);
601
                }
602

603
                internal static async Task ForEachAsync<T>(this IEnumerable<T> items, Func<T, int, Task> action, int maxDegreeOfParalellism)
604
                {
605
                        var allTasks = new List<Task>();
606
                        using var throttler = new SemaphoreSlim(initialCount: maxDegreeOfParalellism);
607
                        foreach (var (item, index) in items.Select((value, i) => (value, i)))
608
                        {
609
                                await throttler.WaitAsync();
610
                                allTasks.Add(
611
                                        Task.Run(async () =>
612
                                        {
613
                                                try
614
                                                {
615
                                                        await action(item, index).ConfigureAwait(false);
616
                                                }
617
                                                finally
618
                                                {
619
                                                        throttler.Release();
620
                                                }
621
                                        }));
622
                        }
623

624
                        await Task.WhenAll(allTasks).ConfigureAwait(false);
625
                }
626

627
                /// <summary>
628
                /// Gets the attribute of the specified type.
629
                /// </summary>
630
                /// <typeparam name="T">The type of the desired attribute.</typeparam>
631
                /// <param name="enumVal">The enum value.</param>
632
                /// <returns>The attribute.</returns>
633
                internal static T GetAttributeOfType<T>(this Enum enumVal)
634
                        where T : Attribute
635
                {
636
                        return enumVal.GetType()
29✔
637
                                .GetTypeInfo()
29✔
638
                                .DeclaredMembers
29✔
639
                                .SingleOrDefault(x => x.Name == enumVal.ToString())
29✔
640
                                ?.GetCustomAttribute<T>(false);
29✔
641
                }
642

643
                /// <summary>
644
                /// Indicates if an object contain a numerical value.
645
                /// </summary>
646
                /// <param name="value">The object.</param>
647
                /// <returns>A boolean indicating if the object contains a numerical value.</returns>
648
                internal static bool IsNumber(this object value)
649
                {
650
                        return value is sbyte
×
651
                                   || value is byte
×
652
                                   || value is short
×
653
                                   || value is ushort
×
654
                                   || value is int
×
655
                                   || value is uint
×
656
                                   || value is long
×
657
                                   || value is ulong
×
658
                                   || value is float
×
659
                                   || value is double
×
660
                                   || value is decimal;
×
661
                }
662

663
                /// <summary>
664
                /// Returns the first value for a specified header stored in the System.Net.Http.Headers.HttpHeaderscollection.
665
                /// </summary>
666
                /// <param name="headers">The HTTP headers.</param>
667
                /// <param name="name">The specified header to return value for.</param>
668
                /// <returns>A string.</returns>
669
                internal static string GetValue(this HttpHeaders headers, string name)
670
                {
671
                        if (headers == null) return null;
9✔
672

673
                        if (headers.TryGetValues(name, out IEnumerable<string> values))
9✔
674
                        {
675
                                return values.FirstOrDefault();
9✔
676
                        }
677

678
                        return null;
×
679
                }
680

681
                internal static IEnumerable<KeyValuePair<string, string>> ParseQuerystring(this Uri uri)
682
                {
683
                        var querystringParameters = uri
×
684
                                .Query.TrimStart('?')
×
685
                                .Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries)
×
686
                                .Select(value => value.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries))
×
687
                                .Select(splitValue =>
×
688
                                {
×
689
                                        if (splitValue.Length == 1)
×
690
                                        {
×
691
                                                return new KeyValuePair<string, string>(splitValue[0].Trim(), null);
×
692
                                        }
×
693
                                        else
×
694
                                        {
×
695
                                                return new KeyValuePair<string, string>(splitValue[0].Trim(), splitValue[1].Trim());
×
696
                                        }
×
697
                                });
×
698

699
                        return querystringParameters;
×
700
                }
701

702
                internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response)
703
                {
704
                        var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME);
×
705
                        DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo);
×
706
                        return diagnosticInfo;
×
707
                }
708

709
                internal static async Task<(bool IsError, string ErrorMessage, int? ErrorCode)> GetErrorMessageAsync(this HttpResponseMessage message)
710
                {
711
                        // Default error code
712
                        int? errorCode = null;
713

714
                        // Default error message
715
                        var errorMessage = $"{(int)message.StatusCode}: {message.ReasonPhrase}";
716

717
                        /*
718
                                In case of an error, the Zoom API returns a JSON string that looks like this:
719
                                {
720
                                        "code": 300,
721
                                        "message": "This meeting has not registration required: 544993922"
722
                                }
723

724
                                Sometimes, the JSON string contains additional info like this example:
725
                                {
726
                                        "code":300,
727
                                        "message":"Validation Failed.",
728
                                        "errors":[
729
                                                {
730
                                                        "field":"settings.jbh_time",
731
                                                        "message":"Invalid parameter: jbh_time."
732
                                                }
733
                                        ]
734
                                }
735
                        */
736

737
                        var responseContent = await message.Content.ReadAsStringAsync(null).ConfigureAwait(false);
738

739
                        if (!string.IsNullOrEmpty(responseContent))
740
                        {
741
                                try
742
                                {
743
                                        var rootJsonElement = JsonDocument.Parse(responseContent).RootElement;
744

745
                                        if (rootJsonElement.ValueKind == JsonValueKind.Object)
746
                                        {
747
                                                errorCode = rootJsonElement.TryGetProperty("code", out JsonElement jsonErrorCode) ? jsonErrorCode.GetInt32() : null;
748
                                                errorMessage = rootJsonElement.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage;
749
                                                if (rootJsonElement.TryGetProperty("errors", out JsonElement jsonErrorDetails))
750
                                                {
751
                                                        var errorDetails = string.Join(
752
                                                                " ",
753
                                                                jsonErrorDetails
754
                                                                        .EnumerateArray()
755
                                                                        .Select(jsonErrorDetail =>
756
                                                {
757
                                                        var errorDetail = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty;
758
                                                        return errorDetail;
759
                                                })
760
                                                                        .Where(message => !string.IsNullOrEmpty(message)));
761

762
                                                        if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}";
763
                                                }
764

765
                                                return (errorCode.HasValue, errorMessage, errorCode);
766
                                        }
767
                                }
768
                                catch
769
                                {
770
                                        // Intentionally ignore parsing errors
771
                                }
772
                        }
773

774
                        return (!message.IsSuccessStatusCode, errorMessage, errorCode);
775
                }
776

777
                internal static async Task<Stream> CompressAsync(this Stream source)
778
                {
779
                        var compressedStream = new MemoryStream();
780
                        using (var gzip = new GZipStream(compressedStream, CompressionMode.Compress, true))
781
                        {
782
                                await source.CopyToAsync(gzip).ConfigureAwait(false);
783
                        }
784

785
                        compressedStream.Position = 0;
786
                        return compressedStream;
787
                }
788

789
                internal static async Task<Stream> DecompressAsync(this Stream source)
790
                {
791
                        var decompressedStream = new MemoryStream();
792
                        using (var gzip = new GZipStream(source, CompressionMode.Decompress, true))
793
                        {
794
                                await gzip.CopyToAsync(decompressedStream).ConfigureAwait(false);
795
                        }
796

797
                        decompressedStream.Position = 0;
798
                        return decompressedStream;
799
                }
800

801
                /// <summary>Convert an enum to its string representation.</summary>
802
                /// <typeparam name="T">The enum type.</typeparam>
803
                /// <param name="enumValue">The value.</param>
804
                /// <returns>The string representation of the enum value.</returns>
805
                /// <remarks>Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .</remarks>
806
                internal static string ToEnumString<T>(this T enumValue)
807
                        where T : Enum
808
                {
809
                        if (enumValue.TryToEnumString(out string stringValue)) return stringValue;
25✔
810
                        return enumValue.ToString();
1✔
811
                }
812

813
                internal static bool TryToEnumString<T>(this T enumValue, out string stringValue)
814
                        where T : Enum
815
                {
816
                        var multipleValuesEnumMemberAttribute = enumValue.GetAttributeOfType<MultipleValuesEnumMemberAttribute>();
13✔
817
                        if (multipleValuesEnumMemberAttribute != null)
13✔
818
                        {
819
                                stringValue = multipleValuesEnumMemberAttribute.DefaultValue;
2✔
820
                                return true;
2✔
821
                        }
822

823
                        var enumMemberAttribute = enumValue.GetAttributeOfType<EnumMemberAttribute>();
11✔
824
                        if (enumMemberAttribute != null)
11✔
825
                        {
826
                                stringValue = enumMemberAttribute.Value;
8✔
827
                                return true;
8✔
828
                        }
829

830
                        var jsonPropertyNameAttribute = enumValue.GetAttributeOfType<JsonPropertyNameAttribute>();
3✔
831
                        if (jsonPropertyNameAttribute != null)
3✔
832
                        {
833
                                stringValue = jsonPropertyNameAttribute.Name;
1✔
834
                                return true;
1✔
835
                        }
836

837
                        var descriptionAttribute = enumValue.GetAttributeOfType<DescriptionAttribute>();
2✔
838
                        if (descriptionAttribute != null)
2✔
839
                        {
840
                                stringValue = descriptionAttribute.Description;
1✔
841
                                return true;
1✔
842
                        }
843

844
                        stringValue = null;
1✔
845
                        return false;
1✔
846
                }
847

848
                /// <summary>Parses a string into its corresponding enum value.</summary>
849
                /// <typeparam name="T">The enum type.</typeparam>
850
                /// <param name="str">The string value.</param>
851
                /// <returns>The enum representation of the string value.</returns>
852
                /// <remarks>Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .</remarks>
853
                internal static T ToEnum<T>(this string str)
854
                        where T : Enum
855
                {
856
                        if (str.TryToEnum(out T enumValue)) return enumValue;
400✔
857

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

861
                internal static bool TryToEnum<T>(this string str, out T enumValue)
862
                        where T : Enum
863
                {
864
                        var enumType = typeof(T);
200✔
865
                        foreach (var name in Enum.GetNames(enumType))
7,032✔
866
                        {
867
                                var customAttributes = enumType.GetField(name).GetCustomAttributes(true);
3,416✔
868

869
                                // See if there's a matching 'MultipleValuesEnumMember' attribute
870
                                if (customAttributes.OfType<MultipleValuesEnumMemberAttribute>().Any(attribute => string.Equals(attribute.DefaultValue, str, StringComparison.OrdinalIgnoreCase) ||
3,416✔
871
                                        (attribute.OtherValues ?? Array.Empty<string>()).Any(otherValue => string.Equals(otherValue, str, StringComparison.OrdinalIgnoreCase))))
3,416✔
872
                                {
873
                                        enumValue = (T)Enum.Parse(enumType, name);
7✔
874
                                        return true;
7✔
875
                                }
876

877
                                // See if there's a matching 'EnumMember' attribute
878
                                if (customAttributes.OfType<EnumMemberAttribute>().Any(attribute => string.Equals(attribute.Value, str, StringComparison.OrdinalIgnoreCase)))
3,409✔
879
                                {
880
                                        enumValue = (T)Enum.Parse(enumType, name);
189✔
881
                                        return true;
189✔
882
                                }
883

884
                                // See if there's a matching 'JsonPropertyName' attribute
885
                                if (customAttributes.OfType<JsonPropertyNameAttribute>().Any(attribute => string.Equals(attribute.Name, str, StringComparison.OrdinalIgnoreCase)))
3,220✔
886
                                {
887
                                        enumValue = (T)Enum.Parse(enumType, name);
1✔
888
                                        return true;
1✔
889
                                }
890

891
                                // See if there's a matching 'Description' attribute
892
                                if (customAttributes.OfType<DescriptionAttribute>().Any(attribute => string.Equals(attribute.Description, str, StringComparison.OrdinalIgnoreCase)))
3,219✔
893
                                {
894
                                        enumValue = (T)Enum.Parse(enumType, name);
1✔
895
                                        return true;
1✔
896
                                }
897

898
                                // See if the value matches the name
899
                                if (string.Equals(name, str, StringComparison.OrdinalIgnoreCase))
3,218✔
900
                                {
901
                                        enumValue = (T)Enum.Parse(enumType, name);
2✔
902
                                        return true;
2✔
903
                                }
904
                        }
905

906
                        enumValue = default;
×
907
                        return false;
×
908
                }
909

910
                internal static T ToObject<T>(this JsonElement element, JsonSerializerOptions options = null)
911
                {
912
                        return JsonSerializer.Deserialize<T>(element.GetRawText(), options ?? JsonFormatter.DeserializerOptions);
22✔
913
                }
914

915
                internal static void Add<T>(this JsonObject jsonObject, string propertyName, T value)
916
                {
917
                        if (value is IEnumerable<T> items)
×
918
                        {
919
                                var jsonArray = new JsonArray();
×
920
                                foreach (var item in items)
×
921
                                {
922
                                        jsonArray.Add(item);
×
923
                                }
924

925
                                jsonObject.Add(propertyName, jsonArray);
×
926
                        }
927
                        else
928
                        {
929
                                jsonObject.Add(propertyName, JsonValue.Create(value));
×
930
                        }
931
                }
×
932

933
                internal static string ToHexString(this byte[] bytes)
934
                {
935
                        var result = new StringBuilder(bytes.Length * 2);
2✔
936
                        for (int i = 0; i < bytes.Length; i++)
132✔
937
                                result.Append(bytes[i].ToString("x2"));
64✔
938
                        return result.ToString();
2✔
939
                }
940

941
                internal static string ToExactLength(this string source, int totalWidth, string postfix = "...", char paddingChar = ' ')
942
                {
943
                        if (string.IsNullOrEmpty(source)) return new string(paddingChar, totalWidth);
×
944
                        if (source.Length <= totalWidth) return source.PadRight(totalWidth, paddingChar);
×
945
                        var result = $"{source.Substring(0, totalWidth - (postfix?.Length ?? 0))}{postfix ?? string.Empty}";
×
946
                        return result;
×
947
                }
948

949
                internal static bool IsNullableType(this Type type)
950
                {
951
                        return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
59✔
952
                }
953

954
                /// <summary>Asynchronously converts the JSON encoded content and convert it to an object of the desired type.</summary>
955
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
956
                /// <param name="httpContent">The content.</param>
957
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
958
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
959
                /// <param name="options">Options to control behavior Converter during parsing.</param>
960
                /// <param name="cancellationToken">The cancellation token.</param>
961
                /// <returns>Returns the strongly typed object.</returns>
962
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
963
                private static async Task<T> AsObject<T>(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
964
                {
965
                        var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
966

967
                        if (string.IsNullOrEmpty(propertyName))
968
                        {
969
                                return JsonSerializer.Deserialize<T>(responseContent, options ?? JsonFormatter.DeserializerOptions);
970
                        }
971

972
                        var jsonDoc = JsonDocument.Parse(responseContent, default);
973
                        if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
974
                        {
975
                                return property.ToObject<T>(options);
976
                        }
977
                        else if (throwIfPropertyIsMissing)
978
                        {
979
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
980
                        }
981
                        else
982
                        {
983
                                return default;
984
                        }
985
                }
986

987
                /// <summary>Get a raw JSON object representation of the response.</summary>
988
                /// <param name="httpContent">The content.</param>
989
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
990
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
991
                /// <param name="cancellationToken">The cancellation token.</param>
992
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
993
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
994
                private static async Task<JsonDocument> AsRawJsonDocument(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, CancellationToken cancellationToken = default)
995
                {
996
                        var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
997

998
                        var jsonDoc = JsonDocument.Parse(responseContent, default);
999

1000
                        if (string.IsNullOrEmpty(propertyName))
1001
                        {
1002
                                return jsonDoc;
1003
                        }
1004

1005
                        if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
1006
                        {
1007
                                var propertyContent = property.GetRawText();
1008
                                return JsonDocument.Parse(propertyContent, default);
1009
                        }
1010
                        else if (throwIfPropertyIsMissing)
1011
                        {
1012
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
1013
                        }
1014
                        else
1015
                        {
1016
                                return default;
1017
                        }
1018
                }
1019

1020
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponse' object.</summary>
1021
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
1022
                /// <param name="httpContent">The content.</param>
1023
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1024
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1025
                /// <param name="cancellationToken">The cancellation token.</param>
1026
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
1027
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1028
                private static async Task<PaginatedResponse<T>> AsPaginatedResponse<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1029
                {
1030
                        // Get the content as a queryable json document
1031
                        var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false);
1032
                        var rootElement = doc.RootElement;
1033

1034
                        // Get the various metadata properties
1035
                        var pageCount = rootElement.GetPropertyValue("page_count", 0);
1036
                        var pageNumber = rootElement.GetPropertyValue("page_number", 0);
1037
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1038
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1039

1040
                        // Get the property that holds the records
1041
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1042

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

1049
                        var result = new PaginatedResponse<T>()
1050
                        {
1051
                                PageCount = pageCount,
1052
                                PageNumber = pageNumber,
1053
                                PageSize = pageSize,
1054
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1055
                        };
1056
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1057

1058
                        return result;
1059
                }
1060

1061
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithToken' object.</summary>
1062
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
1063
                /// <param name="httpContent">The content.</param>
1064
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1065
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1066
                /// <param name="cancellationToken">The cancellation token.</param>
1067
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
1068
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1069
                private static async Task<PaginatedResponseWithToken<T>> AsPaginatedResponseWithToken<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1070
                {
1071
                        // Get the content as a queryable json document
1072
                        var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false);
1073
                        var rootElement = doc.RootElement;
1074

1075
                        // Get the various metadata properties
1076
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1077
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1078
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1079

1080
                        // Get the property that holds the records
1081
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1082

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

1089
                        var result = new PaginatedResponseWithToken<T>()
1090
                        {
1091
                                NextPageToken = nextPageToken,
1092
                                PageSize = pageSize,
1093
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1094
                        };
1095
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1096

1097
                        return result;
1098
                }
1099

1100
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithToken' object.</summary>
1101
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
1102
                /// <param name="httpContent">The content.</param>
1103
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1104
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1105
                /// <param name="cancellationToken">The cancellation token.</param>
1106
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
1107
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1108
                private static async Task<PaginatedResponseWithTokenAndDateRange<T>> AsPaginatedResponseWithTokenAndDateRange<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1109
                {
1110
                        // Get the content as a queryable json document
1111
                        var doc = await httpContent.AsRawJsonDocument(null, false, cancellationToken).ConfigureAwait(false);
1112
                        var rootElement = doc.RootElement;
1113

1114
                        // Get the various metadata properties
1115
                        var from = DateTime.ParseExact(rootElement.GetPropertyValue("from", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1116
                        var to = DateTime.ParseExact(rootElement.GetPropertyValue("to", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1117
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1118
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1119
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1120

1121
                        // Get the property that holds the records
1122
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1123

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

1130
                        var result = new PaginatedResponseWithTokenAndDateRange<T>()
1131
                        {
1132
                                From = from,
1133
                                To = to,
1134
                                NextPageToken = nextPageToken,
1135
                                PageSize = pageSize,
1136
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1137
                        };
1138
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1139

1140
                        return result;
1141
                }
1142

1143
                private static T GetPropertyValue<T>(this JsonElement element, string[] names, T defaultValue, bool throwIfMissing)
1144
                {
1145
                        JsonElement? property = null;
74✔
1146

1147
                        foreach (var name in names)
230✔
1148
                        {
1149
                                property = element.GetProperty(name, false);
74✔
1150
                                if (property.HasValue) break;
74✔
1151
                        }
1152

1153
                        if (!property.HasValue) return defaultValue;
82✔
1154

1155
                        var typeOfT = typeof(T);
66✔
1156

1157
                        if (typeOfT.IsEnum)
66✔
1158
                        {
1159
                                return property.Value.ValueKind switch
7✔
1160
                                {
7✔
1161
                                        JsonValueKind.String => (T)Enum.Parse(typeof(T), property.Value.GetString()),
×
1162
                                        JsonValueKind.Number => (T)Enum.ToObject(typeof(T), property.Value.GetInt16()),
7✔
1163
                                        _ => throw new ArgumentException($"Unable to convert a {property.Value.ValueKind} into a {typeof(T).FullName}", nameof(T)),
×
1164
                                };
7✔
1165
                        }
1166

1167
                        if (typeOfT.IsNullableType())
59✔
1168
                        {
1169
                                if (property.Value.ValueKind == JsonValueKind.Null) return (T)default;
9✔
1170

1171
                                var underlyingType = Nullable.GetUnderlyingType(typeOfT);
9✔
1172
                                var getElementValue = typeof(Internal)
9✔
1173
                                        .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
9✔
1174
                                        .MakeGenericMethod(underlyingType);
9✔
1175

1176
                                return (T)getElementValue.Invoke(null, new object[] { property.Value });
9✔
1177
                        }
1178

1179
                        if (typeOfT.IsArray)
50✔
1180
                        {
1181
                                if (property.Value.ValueKind == JsonValueKind.Null) return (T)default;
1✔
1182

1183
                                var elementType = typeOfT.GetElementType();
1✔
1184
                                var getElementValue = typeof(Internal)
1✔
1185
                                        .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
1✔
1186
                                        .MakeGenericMethod(elementType);
1✔
1187

1188
                                var arrayList = new ArrayList(property.Value.GetArrayLength());
1✔
1189
                                foreach (var arrayElement in property.Value.EnumerateArray())
4✔
1190
                                {
1191
                                        var elementValue = getElementValue.Invoke(null, new object[] { arrayElement });
1✔
1192
                                        arrayList.Add(elementValue);
1✔
1193
                                }
1194

1195
                                return (T)Convert.ChangeType(arrayList.ToArray(elementType), typeof(T));
1✔
1196
                        }
1197

1198
                        return property.Value.GetElementValue<T>();
49✔
1199
                }
1200

1201
                private static T GetElementValue<T>(this JsonElement element)
1202
                {
1203
                        var typeOfT = typeof(T);
59✔
1204

1205
                        if (element.ValueKind == JsonValueKind.Null)
59✔
1206
                        {
1207
                                return typeOfT.IsNullableType()
×
1208
                                        ? (T)default
×
1209
                                        : throw new Exception($"JSON contains a null value but {typeOfT.FullName} is not nullable");
×
1210
                        }
1211

1212
                        return typeOfT switch
59✔
1213
                        {
59✔
1214
                                Type boolType when boolType == typeof(bool) => (T)(object)element.GetBoolean(),
59✔
1215
                                Type strType when strType == typeof(string) => (T)(object)element.GetString(),
90✔
1216
                                Type bytesType when bytesType == typeof(byte[]) => (T)(object)element.GetBytesFromBase64(),
28✔
1217
                                Type sbyteType when sbyteType == typeof(sbyte) => (T)(object)element.GetSByte(),
28✔
1218
                                Type byteType when byteType == typeof(byte) => (T)(object)element.GetByte(),
28✔
1219
                                Type shortType when shortType == typeof(short) => (T)(object)element.GetInt16(),
28✔
1220
                                Type ushortType when ushortType == typeof(ushort) => (T)(object)element.GetUInt16(),
28✔
1221
                                Type intType when intType == typeof(int) => (T)(object)element.GetInt32(),
47✔
1222
                                Type uintType when uintType == typeof(uint) => (T)(object)element.GetUInt32(),
9✔
1223
                                Type longType when longType == typeof(long) => (T)(object)element.GetInt64(),
18✔
1224
                                Type ulongType when ulongType == typeof(ulong) => (T)(object)element.GetUInt64(),
×
1225
                                Type doubleType when doubleType == typeof(double) => (T)(object)element.GetDouble(),
×
1226
                                Type floatType when floatType == typeof(float) => (T)(object)element.GetSingle(),
×
1227
                                Type decimalType when decimalType == typeof(decimal) => (T)(object)element.GetDecimal(),
×
1228
                                Type datetimeType when datetimeType == typeof(DateTime) => (T)(object)element.GetDateTime(),
×
1229
                                Type offsetType when offsetType == typeof(DateTimeOffset) => (T)(object)element.GetDateTimeOffset(),
×
1230
                                Type guidType when guidType == typeof(Guid) => (T)(object)element.GetGuid(),
×
1231
                                _ => throw new ArgumentException($"Unsable to map {typeof(T).FullName} to a corresponding JSON type", nameof(T)),
×
1232
                        };
59✔
1233
                }
1234
        }
1235
}
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