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

Jericho / ZoomNet / 834

15 Nov 2024 03:18PM UTC coverage: 20.194% (-0.2%) from 20.435%
834

push

appveyor

Jericho
Merge branch 'release/0.84.0'

645 of 3194 relevant lines covered (20.19%)

11.81 hits per line

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

58.82
/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;
26✔
220
                        try
221
                        {
222
                                var charset = content?.Headers?.ContentType?.CharSet;
26✔
223
                                if (!string.IsNullOrEmpty(charset))
26✔
224
                                {
225
                                        encoding = Encoding.GetEncoding(charset);
26✔
226
                                }
227
                        }
26✔
228
                        catch
×
229
                        {
230
                                encoding = defaultEncoding;
×
231
                        }
×
232

233
                        return encoding;
26✔
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()
33✔
638
                                .GetTypeInfo()
33✔
639
                                .DeclaredMembers
33✔
640
                                .SingleOrDefault(x => x.Name == enumVal.ToString())
33✔
641
                                ?.GetCustomAttribute<T>(false);
33✔
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 =>
752
                                                                {
753
                                                                        var field = jsonErrorDetail.TryGetProperty("field", out JsonElement jsonField) ? jsonField.GetString() : string.Empty;
754
                                                                        var message = jsonErrorDetail.TryGetProperty("message", out JsonElement jsonErrorMessage) ? jsonErrorMessage.GetString() : string.Empty;
755
                                                                        return $"{field} {message}".Trim();
756
                                                                })
757
                                                                .Where(message => !string.IsNullOrEmpty(message)));
758

759
                                                if (!string.IsNullOrEmpty(errorDetails)) errorMessage += $" {errorDetails}";
760
                                        }
761

762
                                        return (errorCode.HasValue, errorMessage, errorCode);
763
                                }
764
                        }
765
                        catch
766
                        {
767
                                // Intentionally ignore parsing errors
768
                        }
769

770
                        return (!message.IsSuccessStatusCode, errorMessage, errorCode);
771
                }
772

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

781
                        compressedStream.Position = 0;
782
                        return compressedStream;
783
                }
784

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

793
                        decompressedStream.Position = 0;
794
                        return decompressedStream;
795
                }
796

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

809
                internal static bool TryToEnumString<T>(this T enumValue, out string stringValue, bool throwWhenUndefined = true)
810
                        where T : Enum
811
                {
812
                        if (throwWhenUndefined)
15✔
813
                        {
814
                                var typeOfT = typeof(T);
14✔
815
                                if (!Enum.IsDefined(typeOfT, enumValue))
14✔
816
                                {
817
                                        throw new ArgumentException($"{enumValue} is not a valid value for {typeOfT.Name}", nameof(enumValue));
1✔
818
                                }
819
                        }
820

821
                        var multipleValuesEnumMemberAttribute = enumValue.GetAttributeOfType<MultipleValuesEnumMemberAttribute>();
14✔
822
                        if (multipleValuesEnumMemberAttribute != null)
14✔
823
                        {
824
                                stringValue = multipleValuesEnumMemberAttribute.DefaultValue;
2✔
825
                                return true;
2✔
826
                        }
827

828
                        var enumMemberAttribute = enumValue.GetAttributeOfType<EnumMemberAttribute>();
12✔
829
                        if (enumMemberAttribute != null)
12✔
830
                        {
831
                                stringValue = enumMemberAttribute.Value;
8✔
832
                                return true;
8✔
833
                        }
834

835
                        var jsonPropertyNameAttribute = enumValue.GetAttributeOfType<JsonPropertyNameAttribute>();
4✔
836
                        if (jsonPropertyNameAttribute != null)
4✔
837
                        {
838
                                stringValue = jsonPropertyNameAttribute.Name;
1✔
839
                                return true;
1✔
840
                        }
841

842
                        var descriptionAttribute = enumValue.GetAttributeOfType<DescriptionAttribute>();
3✔
843
                        if (descriptionAttribute != null)
3✔
844
                        {
845
                                stringValue = descriptionAttribute.Description;
1✔
846
                                return true;
1✔
847
                        }
848

849
                        stringValue = null;
2✔
850
                        return false;
2✔
851
                }
852

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

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

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

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

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

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

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

903
                                // See if the value matches the name
904
                                if (string.Equals(name, str, StringComparison.OrdinalIgnoreCase))
3,218✔
905
                                {
906
                                        enumValue = (T)Enum.Parse(enumType, name);
2✔
907
                                        return true;
2✔
908
                                }
909
                        }
910

911
                        enumValue = default;
×
912
                        return false;
×
913
                }
914

915
                internal static T ToObject<T>(this JsonElement element, JsonSerializerOptions options = null)
916
                {
917
                        return JsonSerializer.Deserialize<T>(element.GetRawText(), options ?? JsonFormatter.DeserializerOptions);
22✔
918
                }
919

920
                internal static void Add<T>(this JsonObject jsonObject, string propertyName, T value)
921
                {
922
                        if (value is IEnumerable<T> items)
×
923
                        {
924
                                var jsonArray = new JsonArray();
×
925
                                foreach (var item in items)
×
926
                                {
927
                                        jsonArray.Add(item);
×
928
                                }
929

930
                                jsonObject.Add(propertyName, jsonArray);
×
931
                        }
932
                        else
933
                        {
934
                                jsonObject.Add(propertyName, JsonValue.Create(value));
×
935
                        }
936
                }
×
937

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

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

954
                internal static bool IsNullableType(this Type type)
955
                {
956
                        return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
58✔
957
                }
958

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

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

986
                                var prefix = matches.Groups[1].Value;
987
                                var message = matches.Groups[2].Value;
988
                                var postfix = matches.Groups[3].Value;
989
                                if (string.IsNullOrEmpty(message)) throw;
990

991
                                var escapedMessage = Regex.Replace(message, @"(?<!\\)""", "\\\"", RegexOptions.Compiled); // Replace un-escaped double-quotes with properly escaped double-quotes
992
                                var result = $"{prefix}{escapedMessage}{postfix}";
993
                                return JsonDocument.Parse(result).RootElement;
994
                        }
995
                }
996

997
                /// <summary>Asynchronously converts the JSON encoded content and convert it to an object of the desired type.</summary>
998
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
999
                /// <param name="httpContent">The content.</param>
1000
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1001
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
1002
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1003
                /// <param name="cancellationToken">The cancellation token.</param>
1004
                /// <returns>Returns the strongly typed object.</returns>
1005
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1006
                private static async Task<T> AsObject<T>(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1007
                {
1008
                        var responseContent = await httpContent.ReadAsStringAsync(null, cancellationToken).ConfigureAwait(false);
1009

1010
                        if (string.IsNullOrEmpty(propertyName))
1011
                        {
1012
                                return JsonSerializer.Deserialize<T>(responseContent, options ?? JsonFormatter.DeserializerOptions);
1013
                        }
1014

1015
                        var jsonDoc = JsonDocument.Parse(responseContent, default);
1016
                        if (jsonDoc.RootElement.TryGetProperty(propertyName, out JsonElement property))
1017
                        {
1018
                                return property.ToObject<T>(options);
1019
                        }
1020
                        else if (throwIfPropertyIsMissing)
1021
                        {
1022
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
1023
                        }
1024
                        else
1025
                        {
1026
                                return default;
1027
                        }
1028
                }
1029

1030
                /// <summary>Get a JSON representation of the response.</summary>
1031
                /// <param name="httpContent">The content.</param>
1032
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1033
                /// <param name="throwIfPropertyIsMissing">Indicates if an exception should be thrown when the specified JSON property is missing from the response.</param>
1034
                /// <param name="cancellationToken">The cancellation token.</param>
1035
                /// <returns>Returns the response body, or a JsonElement with its 'ValueKind' set to 'Undefined' if the response has no body.</returns>
1036
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1037
                private static async Task<JsonElement> AsJson(this HttpContent httpContent, string propertyName = null, bool throwIfPropertyIsMissing = true, CancellationToken cancellationToken = default)
1038
                {
1039
                        var jsonResponse = await httpContent.ParseZoomResponseAsync(cancellationToken).ConfigureAwait(false);
1040

1041
                        if (string.IsNullOrEmpty(propertyName))
1042
                        {
1043
                                return jsonResponse;
1044
                        }
1045

1046
                        if (jsonResponse.ValueKind != JsonValueKind.Object)
1047
                        {
1048
                                throw new Exception("The response from the Zomm API does not contain a valid JSON string");
1049
                        }
1050
                        else if (jsonResponse.TryGetProperty(propertyName, out JsonElement property))
1051
                        {
1052
                                var propertyContent = property.GetRawText();
1053
                                return JsonDocument.Parse(propertyContent, default).RootElement;
1054
                        }
1055
                        else if (throwIfPropertyIsMissing)
1056
                        {
1057
                                throw new ArgumentException($"The response does not contain a field called '{propertyName}'", nameof(propertyName));
1058
                        }
1059
                        else
1060
                        {
1061
                                return default;
1062
                        }
1063
                }
1064

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

1078
                        // Get the various metadata properties
1079
                        var pageCount = rootElement.GetPropertyValue("page_count", 0);
1080
                        var pageNumber = rootElement.GetPropertyValue("page_number", 0);
1081
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1082
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1083

1084
                        // Get the property that holds the records
1085
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1086

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

1093
                        var result = new PaginatedResponse<T>()
1094
                        {
1095
                                PageCount = pageCount,
1096
                                PageNumber = pageNumber,
1097
                                PageSize = pageSize,
1098
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1099
                        };
1100
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1101

1102
                        return result;
1103
                }
1104

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

1118
                        // Get the various metadata properties
1119
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1120
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1121
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1122

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

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

1132
                        var result = new PaginatedResponseWithToken<T>()
1133
                        {
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
                /// <summary>Asynchronously retrieve the JSON encoded content and convert it to a 'PaginatedResponseWithToken' object.</summary>
1144
                /// <typeparam name="T">The response model to deserialize into.</typeparam>
1145
                /// <param name="httpContent">The content.</param>
1146
                /// <param name="propertyName">The name of the JSON property (or null if not applicable) where the desired data is stored.</param>
1147
                /// <param name="options">Options to control behavior Converter during parsing.</param>
1148
                /// <param name="cancellationToken">The cancellation token.</param>
1149
                /// <returns>Returns the response body, or <c>null</c> if the response has no body.</returns>
1150
                /// <exception cref="ApiException">An error occurred processing the response.</exception>
1151
                private static async Task<PaginatedResponseWithTokenAndDateRange<T>> AsPaginatedResponseWithTokenAndDateRange<T>(this HttpContent httpContent, string propertyName, JsonSerializerOptions options = null, CancellationToken cancellationToken = default)
1152
                {
1153
                        // Get the content as a JSON element
1154
                        var rootElement = await httpContent.AsJson(null, false, cancellationToken).ConfigureAwait(false);
1155

1156
                        // Get the various metadata properties
1157
                        var from = DateTime.ParseExact(rootElement.GetPropertyValue("from", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1158
                        var to = DateTime.ParseExact(rootElement.GetPropertyValue("to", string.Empty), "yyyy-MM-dd", CultureInfo.InvariantCulture);
1159
                        var nextPageToken = rootElement.GetPropertyValue("next_page_token", string.Empty);
1160
                        var pageSize = rootElement.GetPropertyValue("page_size", 0);
1161
                        var totalRecords = rootElement.GetPropertyValue("total_records", (int?)null);
1162

1163
                        // Get the property that holds the records
1164
                        var jsonProperty = rootElement.GetProperty(propertyName, false);
1165

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

1172
                        var result = new PaginatedResponseWithTokenAndDateRange<T>()
1173
                        {
1174
                                From = from,
1175
                                To = to,
1176
                                NextPageToken = nextPageToken,
1177
                                PageSize = pageSize,
1178
                                Records = jsonProperty.HasValue ? jsonProperty.Value.ToObject<T[]>(options) : Array.Empty<T>()
1179
                        };
1180
                        if (totalRecords.HasValue) result.TotalRecords = totalRecords.Value;
1181

1182
                        return result;
1183
                }
1184

1185
                private static T GetPropertyValue<T>(this JsonElement element, string[] names, T defaultValue, bool throwIfMissing)
1186
                {
1187
                        JsonElement? property = null;
73✔
1188

1189
                        foreach (var name in names)
227✔
1190
                        {
1191
                                property = element.GetProperty(name, false);
73✔
1192
                                if (property.HasValue) break;
73✔
1193
                        }
1194

1195
                        if (!property.HasValue)
73✔
1196
                        {
1197
                                if (throwIfMissing) throw new Exception($"Unable to find {string.Join(", ", names)} in the Json document");
8✔
1198
                                else return defaultValue;
8✔
1199
                        }
1200

1201
                        var typeOfT = typeof(T);
65✔
1202

1203
                        if (typeOfT.IsEnum)
65✔
1204
                        {
1205
                                return property.Value.ValueKind switch
7✔
1206
                                {
7✔
1207
                                        JsonValueKind.String => (T)Enum.Parse(typeof(T), property.Value.GetString()),
×
1208
                                        JsonValueKind.Number => (T)Enum.ToObject(typeof(T), property.Value.GetInt16()),
7✔
1209
                                        _ => throw new ArgumentException($"Unable to convert a {property.Value.ValueKind} into a {typeof(T).FullName}", nameof(T)),
×
1210
                                };
7✔
1211
                        }
1212

1213
                        if (typeOfT.IsNullableType())
58✔
1214
                        {
1215
                                if (property.Value.ValueKind == JsonValueKind.Null) return (T)default;
9✔
1216

1217
                                var underlyingType = Nullable.GetUnderlyingType(typeOfT);
9✔
1218
                                var getElementValue = typeof(Internal)
9✔
1219
                                        .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
9✔
1220
                                        .MakeGenericMethod(underlyingType);
9✔
1221

1222
                                return (T)getElementValue.Invoke(null, new object[] { property.Value });
9✔
1223
                        }
1224

1225
                        if (typeOfT.IsArray)
49✔
1226
                        {
1227
                                if (property.Value.ValueKind == JsonValueKind.Null) return (T)default;
1✔
1228

1229
                                var elementType = typeOfT.GetElementType();
1✔
1230
                                var getElementValue = typeof(Internal)
1✔
1231
                                        .GetMethod(nameof(GetElementValue), BindingFlags.Static | BindingFlags.NonPublic)
1✔
1232
                                        .MakeGenericMethod(elementType);
1✔
1233

1234
                                var arrayList = new ArrayList(property.Value.GetArrayLength());
1✔
1235
                                foreach (var arrayElement in property.Value.EnumerateArray())
4✔
1236
                                {
1237
                                        var elementValue = getElementValue.Invoke(null, new object[] { arrayElement });
1✔
1238
                                        arrayList.Add(elementValue);
1✔
1239
                                }
1240

1241
                                return (T)Convert.ChangeType(arrayList.ToArray(elementType), typeof(T));
1✔
1242
                        }
1243

1244
                        return property.Value.GetElementValue<T>();
48✔
1245
                }
1246

1247
                private static T GetElementValue<T>(this JsonElement element)
1248
                {
1249
                        var typeOfT = typeof(T);
58✔
1250

1251
                        if (element.ValueKind == JsonValueKind.Null)
58✔
1252
                        {
1253
                                return typeOfT.IsNullableType()
×
1254
                                        ? (T)default
×
1255
                                        : throw new Exception($"JSON contains a null value but {typeOfT.FullName} is not nullable");
×
1256
                        }
1257

1258
                        return typeOfT switch
58✔
1259
                        {
58✔
1260
                                Type boolType when boolType == typeof(bool) => (T)(object)element.GetBoolean(),
58✔
1261
                                Type strType when strType == typeof(string) => (T)(object)element.GetString(),
88✔
1262
                                Type bytesType when bytesType == typeof(byte[]) => (T)(object)element.GetBytesFromBase64(),
28✔
1263
                                Type sbyteType when sbyteType == typeof(sbyte) => (T)(object)element.GetSByte(),
28✔
1264
                                Type byteType when byteType == typeof(byte) => (T)(object)element.GetByte(),
28✔
1265
                                Type shortType when shortType == typeof(short) => (T)(object)element.GetInt16(),
28✔
1266
                                Type ushortType when ushortType == typeof(ushort) => (T)(object)element.GetUInt16(),
28✔
1267
                                Type intType when intType == typeof(int) => (T)(object)element.GetInt32(),
47✔
1268
                                Type uintType when uintType == typeof(uint) => (T)(object)element.GetUInt32(),
9✔
1269
                                Type longType when longType == typeof(long) => (T)(object)element.GetInt64(),
18✔
1270
                                Type ulongType when ulongType == typeof(ulong) => (T)(object)element.GetUInt64(),
×
1271
                                Type doubleType when doubleType == typeof(double) => (T)(object)element.GetDouble(),
×
1272
                                Type floatType when floatType == typeof(float) => (T)(object)element.GetSingle(),
×
1273
                                Type decimalType when decimalType == typeof(decimal) => (T)(object)element.GetDecimal(),
×
1274
                                Type datetimeType when datetimeType == typeof(DateTime) => (T)(object)element.GetDateTime(),
×
1275
                                Type offsetType when offsetType == typeof(DateTimeOffset) => (T)(object)element.GetDateTimeOffset(),
×
1276
                                Type guidType when guidType == typeof(Guid) => (T)(object)element.GetGuid(),
×
1277
                                _ => throw new ArgumentException($"Unsable to map {typeof(T).FullName} to a corresponding JSON type", nameof(T)),
×
1278
                        };
58✔
1279
                }
1280
        }
1281
}
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