diff --git a/Source/ZoomNet.UnitTests/Extensions/Internal.cs b/Source/ZoomNet.UnitTests/Extensions/Internal.cs
index 46e4dfa7..cf9fca6d 100644
--- a/Source/ZoomNet.UnitTests/Extensions/Internal.cs
+++ b/Source/ZoomNet.UnitTests/Extensions/Internal.cs
@@ -5,7 +5,7 @@
namespace ZoomNet.UnitTests.Utilities
{
- public class InternalTests
+ public class InternalExtensionsTests
{
[Fact]
public void GetProperty_when_property_is_present_and_throwIfMissing_is_true()
diff --git a/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs b/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs
new file mode 100644
index 00000000..16733aa5
--- /dev/null
+++ b/Source/ZoomNet.UnitTests/Models/DashboardParticipant.cs
@@ -0,0 +1,61 @@
+using Newtonsoft.Json;
+using Shouldly;
+using Xunit;
+using ZoomNet.Models;
+
+namespace StrongGrid.UnitTests.Resources
+{
+ public class DashboardParticipantTests
+ {
+ #region FIELDS
+
+ internal const string SINGLE_DASHBOARDPARTICIPANT_JSON = @"{
+ 'id': 'd52f19c548b88490b5d16fcbd38',
+ 'user_id': '32dsfsd4g5gd',
+ 'user_name': 'dojo',
+ 'device': 'Unknown',
+ 'ip_address': '127.0.0.1',
+ 'location': 'New York',
+ 'network_type': 'Wired',
+ 'microphone': 'Plantronics BT600',
+ 'camera': 'FaceTime HD Camera',
+ 'speaker': 'Plantronics BT600',
+ 'data_center': 'SC',
+ 'full_data_center': 'United States;United States (US03_SC CRC)',
+ 'connection_type': 'P2P',
+ 'join_time': '2019-09-07T13:15:02.837Z',
+ 'leave_time': '2019-09-07T13:15:09.837Z',
+ 'share_application': false,
+ 'share_desktop': true,
+ 'share_whiteboard': true,
+ 'recording': false,
+ 'status': 'in_waiting_room',
+ 'pc_name': 'dojo\'s pc',
+ 'domain': 'Dojo-workspace',
+ 'mac_addr': ' 00:0a:95:9d:68:16',
+ 'harddisk_id': 'sed proident in',
+ 'version': '4.4.55383.0716',
+ 'leave_reason': 'Dojo left the meeting.
Reason: Host ended the meeting.',
+ 'sip_uri': 'sip:sipp@10.100.112.140:11029',
+ 'from_sip_uri': 'sip:sipp@10.100.112.140:11029',
+ 'role': 'panelist'
+ }";
+
+ #endregion
+
+ [Fact]
+ public void Parse_json()
+ {
+ // Arrange
+
+ // Act
+ var result = JsonConvert.DeserializeObject(SINGLE_DASHBOARDPARTICIPANT_JSON);
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.Devices.ShouldNotBeNull();
+ result.Devices.Length.ShouldBe(1);
+ result.Devices[0].ShouldBe(ParticipantDevice.Unknown);
+ }
+ }
+}
diff --git a/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs
new file mode 100644
index 00000000..79191ad4
--- /dev/null
+++ b/Source/ZoomNet.UnitTests/Utilities/ParticipantDeviceConverter.cs
@@ -0,0 +1,147 @@
+using Newtonsoft.Json;
+using Shouldly;
+using System;
+using System.IO;
+using System.Text;
+using Xunit;
+using ZoomNet.Models;
+using ZoomNet.Utilities;
+
+namespace StrongGrid.UnitTests.Utilities
+{
+ public class ParticipantDeviceConverterTests
+ {
+ [Fact]
+ public void Properties()
+ {
+ // Act
+ var converter = new ParticipantDeviceConverter();
+
+ // Assert
+ converter.CanRead.ShouldBeTrue();
+ converter.CanWrite.ShouldBeTrue();
+ }
+
+ [Fact]
+ public void CanConvert()
+ {
+ // Act
+ var converter = new ParticipantDeviceConverter();
+
+ // Assert
+ converter.CanConvert(typeof(string)).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void Write_single()
+ {
+ // Arrange
+ var sb = new StringBuilder();
+ var sw = new StringWriter(sb);
+ var writer = new JsonTextWriter(sw);
+
+ var value = new[]
+ {
+ ParticipantDevice.Windows
+ };
+
+ var serializer = new JsonSerializer();
+
+ var converter = new ParticipantDeviceConverter();
+
+ // Act
+ converter.WriteJson(writer, value, serializer);
+ var result = sb.ToString();
+
+ // Assert
+ result.ShouldBe("\"Windows\"");
+ }
+ [Fact]
+ public void Write_multiple()
+ {
+ // Arrange
+ var sb = new StringBuilder();
+ var sw = new StringWriter(sb);
+ var writer = new JsonTextWriter(sw);
+
+ var value = new[]
+ {
+ ParticipantDevice.Unknown,
+ ParticipantDevice.Phone
+ };
+
+ var serializer = new JsonSerializer();
+
+ var converter = new ParticipantDeviceConverter();
+
+ // Act
+ converter.WriteJson(writer, value, serializer);
+ var result = sb.ToString();
+
+ // Assert
+ result.ShouldBe("\"Unknown + Phone\"");
+ }
+
+ [Theory]
+ [InlineDataAttribute("", ParticipantDevice.Unknown)]
+ [InlineDataAttribute("Unknown", ParticipantDevice.Unknown)]
+ [InlineDataAttribute("Android", ParticipantDevice.Android)]
+ [InlineDataAttribute("Phone", ParticipantDevice.Phone)]
+ [InlineDataAttribute("iOs", ParticipantDevice.IOS)]
+ [InlineDataAttribute("H.323/SIP", ParticipantDevice.Sip)]
+ [InlineDataAttribute("Windows", ParticipantDevice.Windows)]
+ public void Read_single(string value, ParticipantDevice expectedValue)
+ {
+ // Arrange
+ var json = $"'{value}'";
+
+ var textReader = new StringReader(json);
+ var jsonReader = new JsonTextReader(textReader);
+ var objectType = (Type)null;
+ var existingValue = (object)null;
+ var serializer = new JsonSerializer();
+
+ var converter = new ParticipantDeviceConverter();
+
+ // Act
+ jsonReader.Read();
+ var result = converter.ReadJson(jsonReader, objectType, existingValue, serializer);
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.ShouldBeOfType();
+
+ var resultAsArray = (ParticipantDevice[])result;
+ resultAsArray.Length.ShouldBe(1);
+ resultAsArray[0].ShouldBe(expectedValue);
+ }
+
+ [Fact]
+ public void Read_multiple()
+ {
+ // Arrange
+ var json = "'Unknown + Phone'";
+
+ var textReader = new StringReader(json);
+ var jsonReader = new JsonTextReader(textReader);
+ var objectType = (Type)null;
+ var existingValue = (object)null;
+ var serializer = new JsonSerializer();
+
+ var converter = new ParticipantDeviceConverter();
+
+ // Act
+ jsonReader.Read();
+ var result = converter.ReadJson(jsonReader, objectType, existingValue, serializer);
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.ShouldBeOfType();
+
+ var resultAsArray = (ParticipantDevice[])result;
+ resultAsArray.Length.ShouldBe(2);
+ resultAsArray[0].ShouldBe(ParticipantDevice.Unknown);
+ resultAsArray[1].ShouldBe(ParticipantDevice.Phone);
+ }
+ }
+}
diff --git a/Source/ZoomNet/Extensions/Internal.cs b/Source/ZoomNet/Extensions/Internal.cs
index 2f16c65d..30a3820f 100644
--- a/Source/ZoomNet/Extensions/Internal.cs
+++ b/Source/ZoomNet/Extensions/Internal.cs
@@ -3,12 +3,14 @@
using Pathoschild.Http.Client;
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
+using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -712,6 +714,49 @@ internal static void Replace(this ICollection collection, T oldValue, T ne
}
}
+ /// Convert an enum to its string representation.
+ /// The enum type.
+ /// The value.
+ /// The string representation of the enum value.
+ /// Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .
+ internal static string ToEnumString(this T enumValue)
+ where T : Enum
+ {
+ var enumMemberAttribute = enumValue.GetAttributeOfType();
+ if (enumMemberAttribute != null) return enumMemberAttribute.Value;
+
+ var descriptionAttribute = enumValue.GetAttributeOfType();
+ if (descriptionAttribute != null) return descriptionAttribute.Description;
+
+ return enumValue.ToString();
+ }
+
+ /// Parses a string into its corresponding enum value.
+ /// The enum type.
+ /// The string value.
+ /// The enum representation of the string value.
+ /// Inspired by: https://stackoverflow.com/questions/10418651/using-enummemberattribute-and-doing-automatic-string-conversions .
+ internal static T ToEnum(this string str)
+ where T : Enum
+ {
+ var enumType = typeof(T);
+ foreach (var name in Enum.GetNames(enumType))
+ {
+ var customAttributes = enumType.GetField(name).GetCustomAttributes(true);
+
+ // See if there's a matching 'EnumMember' attribute
+ if (customAttributes.OfType().Any(attribute => string.Equals(attribute.Value, str, StringComparison.OrdinalIgnoreCase))) return (T)Enum.Parse(enumType, name);
+
+ // See if there's a matching 'Description' attribute
+ if (customAttributes.OfType().Any(attribute => string.Equals(attribute.Description, str, StringComparison.OrdinalIgnoreCase))) return (T)Enum.Parse(enumType, name);
+
+ // See if the value matches the name
+ if (string.Equals(name, str, StringComparison.OrdinalIgnoreCase)) return (T)Enum.Parse(enumType, name);
+ }
+
+ throw new ArgumentException($"There is no value in the {enumType.Name} enum that corresponds to '{str}'.");
+ }
+
/// Asynchronously converts the JSON encoded content and convert it to an object of the desired type.
/// The response model to deserialize into.
/// The content.
diff --git a/Source/ZoomNet/Models/DashboardParticipant.cs b/Source/ZoomNet/Models/DashboardParticipant.cs
index d6fea68e..45ca2447 100644
--- a/Source/ZoomNet/Models/DashboardParticipant.cs
+++ b/Source/ZoomNet/Models/DashboardParticipant.cs
@@ -1,5 +1,6 @@
using Newtonsoft.Json;
using System;
+using ZoomNet.Utilities;
namespace ZoomNet.Models
{
@@ -38,13 +39,14 @@ public class DashboardParticipant
public string UserName { get; set; }
///
- /// Gets or sets the type of device using which the participant joined the meeting.
+ /// Gets or sets the device(s) used by the participant to join the meeting.
///
///
- /// The type of device using which the participant joined the meeting.
+ /// The type of device used by the participant to join the meeting.
///
[JsonProperty(PropertyName = "device")]
- public ParticipantDevice Device { get; set; }
+ [JsonConverter(typeof(ParticipantDeviceConverter))]
+ public ParticipantDevice[] Devices { get; set; }
///
/// Gets or sets the participant’s IP address.
diff --git a/Source/ZoomNet/Models/ParticipantDevice.cs b/Source/ZoomNet/Models/ParticipantDevice.cs
index bb558743..f5749d25 100644
--- a/Source/ZoomNet/Models/ParticipantDevice.cs
+++ b/Source/ZoomNet/Models/ParticipantDevice.cs
@@ -1,19 +1,19 @@
using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
using System.Runtime.Serialization;
+using ZoomNet.Utilities;
namespace ZoomNet.Models
{
///
/// Enumeration to indicate the type of device a participant used to join a meeting.
///
- [JsonConverter(typeof(StringEnumConverter))]
+ [JsonConverter(typeof(ParticipantDeviceConverter))]
public enum ParticipantDevice
{
///
- /// Unknown
+ /// Unknown.
///
- [EnumMember(Value = "")]
+ [EnumMember(Value = "Unknown")]
Unknown,
///
diff --git a/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs b/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs
new file mode 100644
index 00000000..21b0fdd0
--- /dev/null
+++ b/Source/ZoomNet/Utilities/ParticipantDeviceConverter.cs
@@ -0,0 +1,94 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ZoomNet.Models;
+
+namespace ZoomNet.Utilities
+{
+ ///
+ /// Converts a JSON string into and array of devices.
+ ///
+ ///
+ internal class ParticipantDeviceConverter : JsonConverter
+ {
+ ///
+ /// Determines whether this instance can convert the specified object type.
+ ///
+ /// Type of the object.
+ ///
+ /// true if this instance can convert the specified object type; otherwise, false.
+ ///
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(string);
+ }
+
+ ///
+ /// Gets a value indicating whether this can read JSON.
+ ///
+ ///
+ /// true if this can read JSON; otherwise, false.
+ ///
+ public override bool CanRead
+ {
+ get { return true; }
+ }
+
+ ///
+ /// Gets a value indicating whether this can write JSON.
+ ///
+ ///
+ /// true if this can write JSON; otherwise, false.
+ ///
+ public override bool CanWrite
+ {
+ get { return true; }
+ }
+
+ ///
+ /// Writes the JSON representation of the object.
+ ///
+ /// The to write to.
+ /// The value.
+ /// The calling serializer.
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ var stringValue = string.Join(" + ", ((IEnumerable)value).Select(device => device.ToEnumString()));
+ serializer.Serialize(writer, stringValue);
+ }
+
+ ///
+ /// Reads the JSON representation of the object.
+ ///
+ /// The to read from.
+ /// Type of the object.
+ /// The existing value of object being read.
+ /// The calling serializer.
+ ///
+ /// The object value.
+ ///
+ /// Unable to determine the field type.
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.String)
+ {
+ var stringValue = (string)reader.Value;
+ var items = stringValue
+ .Split(new[] { '+' })
+ .Select(item => Convert(item))
+ .ToArray();
+
+ return items;
+ }
+
+ throw new Exception("Unable to convert to ParticipantDevice");
+ }
+
+ private static ParticipantDevice Convert(string deviceAsString)
+ {
+ if (string.IsNullOrWhiteSpace(deviceAsString)) return ParticipantDevice.Unknown;
+ return deviceAsString.Trim().ToEnum();
+ }
+ }
+}