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

Jericho / ZoomNet / 793

26 Oct 2024 12:38PM UTC coverage: 20.493% (+0.1%) from 20.392%
793

push

appveyor

Jericho
Merge branch 'release/0.82.0'

649 of 3167 relevant lines covered (20.49%)

11.89 hits per line

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

58.12
/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.Text.RegularExpressions;
20
using System.Threading;
21
using System.Threading.Tasks;
22
using ZoomNet.Json;
23
using ZoomNet.Models;
24
using ZoomNet.Utilities;
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
                private static readonly int DEFAULT_DEGREE_OF_PARALLELISM = Environment.ProcessorCount > 1 ? Environment.ProcessorCount / 2 : 1;
1✔
41

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

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

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

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

105
                        if (dateOnly)
8✔
106
                        {
107
                                if (timeZone.HasValue && timeZone.Value == TimeZones.UTC) return date.ToUniversalTime().ToString(dateOnlyFormat);
7✔
108
                                else return date.ToString(dateOnlyFormat);
5✔
109
                        }
110
                        else
111
                        {
112
                                if (timeZone.HasValue && timeZone.Value == TimeZones.UTC) return date.ToUniversalTime().ToString(utcDateFormat);
3✔
113
                                else return date.ToString(defaultDateFormat);
1✔
114
                        }
115
                }
116

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

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

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

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

190
                        return content;
191
                }
192

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

233
                        return encoding;
25✔
234
                }
235

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

499
                        return property;
82✔
500
                }
×
501

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

679
                        return null;
×
680
                }
681

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

700
                        return querystringParameters;
×
701
                }
702

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

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

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

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

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

738
                        try
739
                        {
740
                                var jsonResponse = await message.Content.ParseZoomResponseAsync().ConfigureAwait(false);
741
                                if (jsonResponse.ValueKind == JsonValueKind.Object)
742
                                {
743
                                        errorCode = jsonResponse.TryGetProperty("code", out JsonElement jsonErrorCode) ? jsonErrorCode.GetInt32() : null;
744
                                        errorMessage = jsonResponse.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : errorCode.HasValue ? $"Error code: {errorCode}" : errorMessage;
745
                                        if (jsonResponse.TryGetProperty("errors", out JsonElement jsonErrorDetails))
746
                                        {
747
                                                var errorDetails = string.Join(
748
                                                        " ",
749
                                                        jsonErrorDetails
750
                                                                .EnumerateArray()
751
                                                                .Select(jsonErrorDetail => jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty)
752
                                                                .Where(message => !string.IsNullOrEmpty(message)));
753

754
                                                if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}";
755
                                        }
756

757
                                        return (errorCode.HasValue, errorMessage, errorCode);
758
                                }
759
                        }
760
                        catch
761
                        {
762
                                // Intentionally ignore parsing errors
763
                        }
764

765
                        return (!message.IsSuccessStatusCode, errorMessage, errorCode);
766
                }
767

768
                internal static async Task<Stream> CompressAsync(this Stream source)
769
                {
770
                        var compressedStream = new MemoryStream();
771
                        using (var gzip = new GZipStream(compressedStream, CompressionMode.Compress, true))
772
                        {
773
                                await source.CopyToAsync(gzip).ConfigureAwait(false);
774
                        }
775

776
                        compressedStream.Position = 0;
777
                        return compressedStream;
778
                }
779

780
                internal static async Task<Stream> DecompressAsync(this Stream source)
781
                {
782
                        var decompressedStream = new MemoryStream();
783
                        using (var gzip = new GZipStream(source, CompressionMode.Decompress, true))
784
                        {
785
                                await gzip.CopyToAsync(decompressedStream).ConfigureAwait(false);
786
                        }
787

788
                        decompressedStream.Position = 0;
789
                        return decompressedStream;
790
                }
791

792
                /// <summary>Convert an enum to its string representation.</summary>
793
                /// <typeparam name="T">The enum type.</typeparam>
794
                /// <param name="enumValue">The value.</param>
795
                /// <returns>The string representation of the enum value.</returns>
796
                /// <remarks>Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .</remarks>
797
                internal static string ToEnumString<T>(this T enumValue)
798
                        where T : Enum
799
                {
800
                        if (enumValue.TryToEnumString(out string stringValue)) return stringValue;
25✔
801
                        return enumValue.ToString();
1✔
802
                }
803

804
                internal static bool TryToEnumString<T>(this T enumValue, out string stringValue)
805
                        where T : Enum
806
                {
807
                        var multipleValuesEnumMemberAttribute = enumValue.GetAttributeOfType<MultipleValuesEnumMemberAttribute>();
13✔
808
                        if (multipleValuesEnumMemberAttribute != null)
13✔
809
                        {
810
                                stringValue = multipleValuesEnumMemberAttribute.DefaultValue;
2✔
811
                                return true;
2✔
812
                        }
813

814
                        var enumMemberAttribute = enumValue.GetAttributeOfType<EnumMemberAttribute>();
11✔
815
                        if (enumMemberAttribute != null)
11✔
816
                        {
817
                                stringValue = enumMemberAttribute.Value;
8✔
818
                                return true;
8✔
819
                        }
820

821
                        var jsonPropertyNameAttribute = enumValue.GetAttributeOfType<JsonPropertyNameAttribute>();
3✔
822
                        if (jsonPropertyNameAttribute != null)
3✔
823
                        {
824
                                stringValue = jsonPropertyNameAttribute.Name;
1✔
825
                                return true;
1✔
826
                        }
827

828
                        var descriptionAttribute = enumValue.GetAttributeOfType<DescriptionAttribute>();
2✔
829
                        if (descriptionAttribute != null)
2✔
830
                        {
831
                                stringValue = descriptionAttribute.Description;
1✔
832
                                return true;
1✔
833
                        }
834

835
                        stringValue = null;
1✔
836
                        return false;
1✔
837
                }
838

839
                /// <summary>Parses a string into its corresponding enum value.</summary>
840
                /// <typeparam name="T">The enum type.</typeparam>
841
                /// <param name="str">The string value.</param>
842
                /// <returns>The enum representation of the string value.</returns>
843
                /// <remarks>Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .</remarks>
844
                internal static T ToEnum<T>(this string str)
845
                        where T : Enum
846
                {
847
                        if (str.TryToEnum(out T enumValue)) return enumValue;
400✔
848

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

852
                internal static bool TryToEnum<T>(this string str, out T enumValue)
853
                        where T : Enum
854
                {
855
                        var enumType = typeof(T);
200✔
856
                        foreach (var name in Enum.GetNames(enumType))
7,032✔
857
                        {
858
                                var customAttributes = enumType.GetField(name).GetCustomAttributes(true);
3,416✔
859

860
                                // See if there's a matching 'MultipleValuesEnumMember' attribute
861
                                if (customAttributes.OfType<MultipleValuesEnumMemberAttribute>().Any(attribute => string.Equals(attribute.DefaultValue, str, StringComparison.OrdinalIgnoreCase) ||
3,416✔
862
                                        (attribute.OtherValues ?? Array.Empty<string>()).Any(otherValue => string.Equals(otherValue, str, StringComparison.OrdinalIgnoreCase))))
3,416✔
863
                                {
864
                                        enumValue = (T)Enum.Parse(enumType, name);
7✔
865
                                        return true;
7✔
866
                                }
867

868
                                // See if there's a matching 'EnumMember' attribute
869
                                if (customAttributes.OfType<EnumMemberAttribute>().Any(attribute => string.Equals(attribute.Value, str, StringComparison.OrdinalIgnoreCase)))
3,409✔
870
                                {
871
                                        enumValue = (T)Enum.Parse(enumType, name);
189✔
872
                                        return true;
189✔
873
                                }
874

875
                                // See if there's a matching 'JsonPropertyName' attribute
876
                                if (customAttributes.OfType<JsonPropertyNameAttribute>().Any(attribute => string.Equals(attribute.Name, str, StringComparison.OrdinalIgnoreCase)))
3,220✔
877
                                {
878
                                        enumValue = (T)Enum.Parse(enumType, name);
1✔
879
                                        return true;
1✔
880
                                }
881

882
                                // See if there's a matching 'Description' attribute
883
                                if (customAttributes.OfType<DescriptionAttribute>().Any(attribute => string.Equals(attribute.Description, str, StringComparison.OrdinalIgnoreCase)))
3,219✔
884
                                {
885
                                        enumValue = (T)Enum.Parse(enumType, name);
1✔
886
                                        return true;
1✔
887
                                }
888

889
                                // See if the value matches the name
890
                                if (string.Equals(name, str, StringComparison.OrdinalIgnoreCase))
3,218✔
891
                                {
892
                                        enumValue = (T)Enum.Parse(enumType, name);
2✔
893
                                        return true;
2✔
894
                                }
895
                        }
896

897
                        enumValue = default;
×
898
                        return false;
×
899
                }
900

901
                internal static T ToObject<T>(this JsonElement element, JsonSerializerOptions options = null)
902
                {
903
                        return JsonSerializer.Deserialize<T>(element.GetRawText(), options ?? JsonFormatter.DeserializerOptions);
22✔
904
                }
905

906
                internal static void Add<T>(this JsonObject jsonObject, string propertyName, T value)
907
                {
908
                        if (value is IEnumerable<T> items)
×
909
                        {
910
                                var jsonArray = new JsonArray();
×
911
                                foreach (var item in items)
×
912
                                {
913
                                        jsonArray.Add(item);
×
914
                                }
915

916
                                jsonObject.Add(propertyName, jsonArray);
×
917
                        }
918
                        else
919
                        {
920
                                jsonObject.Add(propertyName, JsonValue.Create(value));
×
921
                        }
922
                }
×
923

924
                internal static string ToHexString(this byte[] bytes)
925
                {
926
                        var result = new StringBuilder(bytes.Length * 2);
2✔
927
                        for (int i = 0; i < bytes.Length; i++)
132✔
928
                                result.Append(bytes[i].ToString("x2"));
64✔
929
                        return result.ToString();
2✔
930
                }
931

932
                internal static string ToExactLength(this string source, int totalWidth, string postfix = "...", char paddingChar = ' ')
933
                {
934
                        if (string.IsNullOrEmpty(source)) return new string(paddingChar, totalWidth);
×
935
                        if (source.Length <= totalWidth) return source.PadRight(totalWidth, paddingChar);
×
936
                        var result = $"{source.Substring(0, totalWidth - (postfix?.Length ?? 0))}{postfix ?? string.Empty}";
×
937
                        return result;
×
938
                }
939

940
                internal static bool IsNullableType(this Type type)
941
                {
942
                        return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
58✔
943
                }
944

945
                private static async Task<JsonElement> ParseZoomResponseAsync(this HttpContent responseFromZoomApi, CancellationToken cancellationToken = default)
946
                {
947
                        var responseContent = await responseFromZoomApi.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
948
                        if (string.IsNullOrEmpty(responseContent)) return default; // FYI: the 'ValueKind' property of the default JsonElement is JsonValueKind.Undefined
949

950
                        try
951
                        {
952
                                // Attempt to parse the response with the assumption that JSON is well-formed
953
                                // If the JSON is malformed, a JsonException will be thrown
954
                                return JsonDocument.Parse(responseContent).RootElement;
955
                        }
956
                        catch (JsonException)
957
                        {
958
                                /*
959
                                        Sometimes the error message is malformed due to the presence of double quotes that are not properly escaped.
960
                                        See: https://devforum.zoom.us/t/list-events-endpoint-returns-invalid-json-in-the-payload/115792 for more info.
961
                                        One instance where this problem was observed is when retrieving the list of events without having the necessary permissions to do so.
962
                                        The result is the following response with unescaped double-quotes in the error message:
963
                                        {
964
                                                "code": 104,
965
                                                "message": "Invalid access token, does not contain scopes:["zoom_events_basic:read","zoom_events_basic:read:admin"]"
966
                                        }
967
                                */
968
                                const string pattern = @"(.*?)(?<=""message"":"")(.*?)(?=""})(.*?$)";
969
                                var matches = Regex.Match(responseContent, pattern, RegexOptions.Compiled | RegexOptions.Singleline);
970
                                if (matches.Groups.Count != 4) throw;
971

972
                                var prefix = matches.Groups[1].Value;
973
                                var message = matches.Groups[2].Value;
974
                                var postfix = matches.Groups[3].Value;
975
                                if (string.IsNullOrEmpty(message)) throw;
976

977
                                var escapedMessage = Regex.Replace(message, @"(?<!\\)""", "\\\"", RegexOptions.Compiled); // Replace un-escaped double-quotes with properly escaped double-quotes
978
                                var result = $"{prefix}{escapedMessage}{postfix}";
979
                                return JsonDocument.Parse(result).RootElement;
980
                        }
981
                }
982

983
                /// <summary>Asynchronously converts the JSON encoded content and convert it to an object of the desired type.</summary>
984
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
985
                /// <param name="httpContent">The content.</param>
986
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
987
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
988
                /// <param name="options">Options to control behavior Converter during parsing.</param>
989
                /// <param name="cancellationToken">The cancellation token.</param>
990
                /// <returns>Returns the strongly typed object.</returns>
991
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
992
                private static async Task<T> AsObject<T>(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
993
                {
994
                        var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
995

996
                        if (string.IsNullOrEmpty(propertyName))
997
                        {
998
                                return JsonSerializer.Deserialize<T>(responseContent, options ?? JsonFormatter.DeserializerOptions);
999
                        }
1000

1001
                        var jsonDoc = JsonDocument.Parse(responseContent, default);
1002
                        if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
1003
                        {
1004
                                return property.ToObject<T>(options);
1005
                        }
1006
                        else if (throwIfPropertyIsMissing)
1007
                        {
1008
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
1009
                        }
1010
                        else
1011
                        {
1012
                                return default;
1013
                        }
1014
                }
1015

1016
                /// <summary>Get a JSON representation of the response.</summary>
1017
                /// <param name="httpContent">The content.</param>
1018
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1019
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
1020
                /// <param name="cancellationToken">The cancellation token.</param>
1021
                /// <returns>Returns the response body, or a JsonElement with its 'ValueKind' set to 'Undefined' if the response has no body.</returns>
1022
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1023
                private static async Task<JsonElement> AsJson(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, CancellationToken cancellationToken = default)
1024
                {
1025
                        var jsonResponse = await httpContent.ParseZoomResponseAsync(cancellationToken).ConfigureAwait(false);
1026

1027
                        if (string.IsNullOrEmpty(propertyName))
1028
                        {
1029
                                return jsonResponse;
1030
                        }
1031

1032
                        if (jsonResponse.ValueKind != JsonValueKind.Object)
1033
                        {
1034
                                throw new Exception("The response from the Zomm API does not contain a valid JSON string");
1035
                        }
1036
                        else if (jsonResponse.TryGetProperty(propertyName, out JsonElement property))
1037
                        {
1038
                                var propertyContent = property.GetRawText();
1039
                                return JsonDocument.Parse(propertyContent, default).RootElement;
1040
                        }
1041
                        else if (throwIfPropertyIsMissing)
1042
                        {
1043
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
1044
                        }
1045
                        else
1046
                        {
1047
                                return default;
1048
                        }
1049
                }
1050

1051
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponse' object.</summary>
1052
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
1053
                /// <param name="httpContent">The content.</param>
1054
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1055
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1056
                /// <param name="cancellationToken">The cancellation token.</param>
1057
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
1058
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1059
                private static async Task<PaginatedResponse<T>> AsPaginatedResponse<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1060
                {
1061
                        // Get the content as a JSON element
1062
                        var rootElement = await httpContent.AsJson(null, false, cancellationToken).ConfigureAwait(false);
1063

1064
                        // Get the various metadata properties
1065
                        var pageCount = rootElement.GetPropertyValue("page_count", 0);
1066
                        var pageNumber = rootElement.GetPropertyValue("page_number", 0);
1067
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1068
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1069

1070
                        // Get the property that holds the records
1071
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1072

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

1079
                        var result = new PaginatedResponse<T>()
1080
                        {
1081
                                PageCount = pageCount,
1082
                                PageNumber = pageNumber,
1083
                                PageSize = pageSize,
1084
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1085
                        };
1086
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1087

1088
                        return result;
1089
                }
1090

1091
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithToken' object.</summary>
1092
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
1093
                /// <param name="httpContent">The content.</param>
1094
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1095
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1096
                /// <param name="cancellationToken">The cancellation token.</param>
1097
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
1098
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1099
                private static async Task<PaginatedResponseWithToken<T>> AsPaginatedResponseWithToken<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1100
                {
1101
                        // Get the content as a JSON element
1102
                        var rootElement = await httpContent.AsJson(null, false, cancellationToken).ConfigureAwait(false);
1103

1104
                        // Get the various metadata properties
1105
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1106
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1107
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1108

1109
                        // Get the property that holds the records
1110
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1111

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

1118
                        var result = new PaginatedResponseWithToken<T>()
1119
                        {
1120
                                NextPageToken = nextPageToken,
1121
                                PageSize = pageSize,
1122
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1123
                        };
1124
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1125

1126
                        return result;
1127
                }
1128

1129
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithToken' object.</summary>
1130
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
1131
                /// <param name="httpContent">The content.</param>
1132
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1133
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1134
                /// <param name="cancellationToken">The cancellation token.</param>
1135
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
1136
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1137
                private static async Task<PaginatedResponseWithTokenAndDateRange<T>> AsPaginatedResponseWithTokenAndDateRange<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1138
                {
1139
                        // Get the content as a JSON element
1140
                        var rootElement = await httpContent.AsJson(null, false, cancellationToken).ConfigureAwait(false);
1141

1142
                        // Get the various metadata properties
1143
                        var from = DateTime.ParseExact(rootElement.GetPropertyValue("from", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1144
                        var to = DateTime.ParseExact(rootElement.GetPropertyValue("to", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1145
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1146
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1147
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1148

1149
                        // Get the property that holds the records
1150
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1151

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

1158
                        var result = new PaginatedResponseWithTokenAndDateRange<T>()
1159
                        {
1160
                                From = from,
1161
                                To = to,
1162
                                NextPageToken = nextPageToken,
1163
                                PageSize = pageSize,
1164
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1165
                        };
1166
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1167

1168
                        return result;
1169
                }
1170

1171
                private static T GetPropertyValue<T>(this JsonElement element, string[] names, T defaultValue, bool throwIfMissing)
1172
                {
1173
                        JsonElement? property = null;
73✔
1174

1175
                        foreach (var name in names)
227✔
1176
                        {
1177
                                property = element.GetProperty(name, false);
73✔
1178
                                if (property.HasValue) break;
73✔
1179
                        }
1180

1181
                        if (!property.HasValue)
73✔
1182
                        {
1183
                                if (throwIfMissing) throw new Exception($"Unable to find {string.Join(", ", names)} in the Json document");
8✔
1184
                                else return defaultValue;
8✔
1185
                        }
1186

1187
                        var typeOfT = typeof(T);
65✔
1188

1189
                        if (typeOfT.IsEnum)
65✔
1190
                        {
1191
                                return property.Value.ValueKind switch
7✔
1192
                                {
7✔
1193
                                        JsonValueKind.String => (T)Enum.Parse(typeof(T), property.Value.GetString()),
×
1194
                                        JsonValueKind.Number => (T)Enum.ToObject(typeof(T), property.Value.GetInt16()),
7✔
1195
                                        _ => throw new ArgumentException($"Unable to convert a {property.Value.ValueKind} into a {typeof(T).FullName}", nameof(T)),
×
1196
                                };
7✔
1197
                        }
1198

1199
                        if (typeOfT.IsNullableType())
58✔
1200
                        {
1201
                                if (property.Value.ValueKind == JsonValueKind.Null) return (T)default;
9✔
1202

1203
                                var underlyingType = Nullable.GetUnderlyingType(typeOfT);
9✔
1204
                                var getElementValue = typeof(Internal)
9✔
1205
                                        .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
9✔
1206
                                        .MakeGenericMethod(underlyingType);
9✔
1207

1208
                                return (T)getElementValue.Invoke(null, new object[] { property.Value });
9✔
1209
                        }
1210

1211
                        if (typeOfT.IsArray)
49✔
1212
                        {
1213
                                if (property.Value.ValueKind == JsonValueKind.Null) return (T)default;
1✔
1214

1215
                                var elementType = typeOfT.GetElementType();
1✔
1216
                                var getElementValue = typeof(Internal)
1✔
1217
                                        .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
1✔
1218
                                        .MakeGenericMethod(elementType);
1✔
1219

1220
                                var arrayList = new ArrayList(property.Value.GetArrayLength());
1✔
1221
                                foreach (var arrayElement in property.Value.EnumerateArray())
4✔
1222
                                {
1223
                                        var elementValue = getElementValue.Invoke(null, new object[] { arrayElement });
1✔
1224
                                        arrayList.Add(elementValue);
1✔
1225
                                }
1226

1227
                                return (T)Convert.ChangeType(arrayList.ToArray(elementType), typeof(T));
1✔
1228
                        }
1229

1230
                        return property.Value.GetElementValue<T>();
48✔
1231
                }
1232

1233
                private static T GetElementValue<T>(this JsonElement element)
1234
                {
1235
                        var typeOfT = typeof(T);
58✔
1236

1237
                        if (element.ValueKind == JsonValueKind.Null)
58✔
1238
                        {
1239
                                return typeOfT.IsNullableType()
×
1240
                                        ? (T)default
×
1241
                                        : throw new Exception($"JSON contains a null value but {typeOfT.FullName} is not nullable");
×
1242
                        }
1243

1244
                        return typeOfT switch
58✔
1245
                        {
58✔
1246
                                Type boolType when boolType == typeof(bool) => (T)(object)element.GetBoolean(),
58✔
1247
                                Type strType when strType == typeof(string) => (T)(object)element.GetString(),
88✔
1248
                                Type bytesType when bytesType == typeof(byte[]) => (T)(object)element.GetBytesFromBase64(),
28✔
1249
                                Type sbyteType when sbyteType == typeof(sbyte) => (T)(object)element.GetSByte(),
28✔
1250
                                Type byteType when byteType == typeof(byte) => (T)(object)element.GetByte(),
28✔
1251
                                Type shortType when shortType == typeof(short) => (T)(object)element.GetInt16(),
28✔
1252
                                Type ushortType when ushortType == typeof(ushort) => (T)(object)element.GetUInt16(),
28✔
1253
                                Type intType when intType == typeof(int) => (T)(object)element.GetInt32(),
47✔
1254
                                Type uintType when uintType == typeof(uint) => (T)(object)element.GetUInt32(),
9✔
1255
                                Type longType when longType == typeof(long) => (T)(object)element.GetInt64(),
18✔
1256
                                Type ulongType when ulongType == typeof(ulong) => (T)(object)element.GetUInt64(),
×
1257
                                Type doubleType when doubleType == typeof(double) => (T)(object)element.GetDouble(),
×
1258
                                Type floatType when floatType == typeof(float) => (T)(object)element.GetSingle(),
×
1259
                                Type decimalType when decimalType == typeof(decimal) => (T)(object)element.GetDecimal(),
×
1260
                                Type datetimeType when datetimeType == typeof(DateTime) => (T)(object)element.GetDateTime(),
×
1261
                                Type offsetType when offsetType == typeof(DateTimeOffset) => (T)(object)element.GetDateTimeOffset(),
×
1262
                                Type guidType when guidType == typeof(Guid) => (T)(object)element.GetGuid(),
×
1263
                                _ => throw new ArgumentException($"Unsable to map {typeof(T).FullName} to a corresponding JSON type", nameof(T)),
×
1264
                        };
58✔
1265
                }
1266
        }
1267
}
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