diff --git a/nanoFramework.Json.Test/JsonUnitTests.cs b/nanoFramework.Json.Test/JsonUnitTests.cs index ea06530..e7fe5a3 100644 --- a/nanoFramework.Json.Test/JsonUnitTests.cs +++ b/nanoFramework.Json.Test/JsonUnitTests.cs @@ -501,14 +501,72 @@ public void SerializeAbstractClassTest() } [TestMethod] - public void CanSerializeAndDeserializeTwinProperties_01() + public void CanDeserializeAzureTwinProperties_01() { - var testString = "{\"desired\":{\"TimeToSleep\":5,\"$version\":2},\"reported\":{\"Firmware\":\"nanoFramework\",\"TimeToSleep\":2,\"$version\":94}}"; - var dserResult = (TwinProperties)JsonConvert.DeserializeObject(testString, typeof(TwinProperties)); + var twinPayload = (TwinProperties)JsonConvert.DeserializeObject(testString, typeof(TwinProperties)); - Assert.NotNull(dserResult, "Deserialization returned a null object"); + Assert.NotNull(twinPayload, "Deserialization returned a null object"); + + Assert.Equal(twinPayload.desired.TimeToSleep, 5, "desired.TimeToSleep doesn't match"); + Assert.Null(twinPayload.desired._metadata, "desired._metadata doesn't match"); + + Assert.Equal(twinPayload.reported.Firmware, "nanoFramework", "reported.Firmware doesn't match"); + Assert.Equal(twinPayload.reported.TimeToSleep, 2, "reported.TimeToSleep doesn't match"); + Assert.Null(twinPayload.reported._metadata, "reported._metadata doesn't match"); + + Debug.WriteLine(""); + } + + [TestMethod] + public void CanDeserializeAzureTwinProperties_02() + { + TwinPayload twinPayload = (TwinPayload)JsonConvert.DeserializeObject(s_AzureTwinsJsonTestPayload, typeof(TwinPayload)); + + Assert.NotNull(twinPayload, "Deserialization returned a null object"); + + Assert.Equal(twinPayload.authenticationType, "sas", "authenticationType doesn't match"); + Assert.Equal(twinPayload.statusUpdateTime.Ticks, DateTime.MinValue.Ticks, "statusUpdateTime doesn't match"); + Assert.Equal(twinPayload.cloudToDeviceMessageCount, 0, "cloudToDeviceMessageCount doesn't match"); + Assert.Equal(twinPayload.x509Thumbprint.Count, 2, "x509Thumbprint collection count doesn't match"); + Assert.Equal(twinPayload.version, 381, "version doesn't match"); + Assert.Equal(twinPayload.properties.desired.TimeToSleep, 30, "properties.desired.TimeToSleep doesn't match"); + Assert.Equal(twinPayload.properties.reported._metadata.Count, 3, "properties.reported._metadata collection count doesn't match"); + Assert.Equal(twinPayload.properties.desired._metadata.Count, 3, "properties.desired._metadata collection count doesn't match"); + + Debug.WriteLine(""); + } + + [TestMethod] + public void CanDeserializeAzureTwinProperties_03() + { + TwinPayload twinPayload = (TwinPayload)JsonConvert.DeserializeObject(s_AzureTwinsJsonTestPayload, typeof(TwinPayload)); + + Assert.NotNull(twinPayload, "Deserialization returned a null object"); + + Assert.Equal(twinPayload.authenticationType, "sas", "authenticationType doesn't match"); + Assert.Equal(twinPayload.statusUpdateTime.Ticks, DateTime.MinValue.Ticks, "statusUpdateTime doesn't match"); + Assert.Equal(twinPayload.cloudToDeviceMessageCount, 0, "cloudToDeviceMessageCount doesn't match"); + Assert.Equal(twinPayload.x509Thumbprint.Count, 2, "x509Thumbprint collection count doesn't match"); + Assert.Equal(twinPayload.version, 381, "version doesn't match"); + Assert.Equal(twinPayload.properties.desired.TimeToSleep, 30, "properties.desired.TimeToSleep doesn't match"); + Assert.Equal(twinPayload.properties.reported._metadata.Count, 3, "properties.reported._metadata collection count doesn't match"); + Assert.Equal(twinPayload.properties.desired._metadata.Count, 3, "properties.desired._metadata collection count doesn't match"); + + Debug.WriteLine(""); + } + + [TestMethod] + public void CanDeserializeAzureTwinProperties_04() + { + TwinPayloadProperties twinPayload = (TwinPayloadProperties)JsonConvert.DeserializeObject(s_AzureTwinsJsonTestPayload, typeof(TwinPayloadProperties)); + + Assert.NotNull(twinPayload, "Deserialization returned a null object"); + + Assert.Equal(twinPayload.properties.desired.TimeToSleep, 30, "properties.desired.TimeToSleep doesn't match"); + Assert.Equal(twinPayload.properties.reported._metadata.Count, 3, "properties.reported._metadata collection count doesn't match"); + Assert.Equal(twinPayload.properties.desired._metadata.Count, 3, "properties.desired._metadata collection count doesn't match"); Debug.WriteLine(""); } @@ -560,15 +618,86 @@ public void CanDeserializeInvocationReceiveMessage_02() #region Test classes + private static string s_AzureTwinsJsonTestPayload = @"{ + ""deviceId"": ""nanoDeepSleep"", + ""etag"": ""AAAAAAAAAAc="", + ""deviceEtag"": ""Njc2MzYzMTQ5"", + ""status"": ""enabled"", + ""statusUpdateTime"": ""0001-01-01T00:00:00Z"", + ""connectionState"": ""Disconnected"", + ""lastActivityTime"": ""2021-06-03T05:52:41.4683112Z"", + ""cloudToDeviceMessageCount"": 0, + ""authenticationType"": ""sas"", + ""x509Thumbprint"": { + ""primaryThumbprint"": null, + ""secondaryThumbprint"": null + }, + ""modelId"": """", + ""version"": 381, + ""properties"": { + ""desired"": { + ""TimeToSleep"": 30, + ""$metadata"": { + ""$lastUpdated"": ""2021-06-03T05:37:11.8120413Z"", + ""$lastUpdatedVersion"": 7, + ""TimeToSleep"": { + ""$lastUpdated"": ""2021-06-03T05:37:11.8120413Z"", + ""$lastUpdatedVersion"": 7 + } + }, + ""$version"": 7 + }, + ""reported"": { + ""Firmware"": ""nanoFramework"", + ""TimeToSleep"": 30, + ""$metadata"": { + ""$lastUpdated"": ""2021-06-03T05:52:41.1232797Z"", + ""Firmware"": { + ""$lastUpdated"": ""2021-06-03T05:52:41.1232797Z"" + }, + ""TimeToSleep"": { + ""$lastUpdated"": ""2021-06-03T05:52:41.1232797Z"" + } + }, + ""$version"": 374 + } + }, + ""capabilities"": { + ""iotEdge"": false + } + }"; + + public class TwinPayload + { + public string deviceId { get; set; } + public string etag { get; set; } + public string status { get; set; } + public DateTime statusUpdateTime { get; set; } + public string connectionState { get; set; } + public DateTime lastActivityTime { get; set; } + public int cloudToDeviceMessageCount { get; set; } + public string authenticationType { get; set; } + public Hashtable x509Thumbprint { get; set; } + public string modelId { get; set; } + public int version { get; set; } + public TwinProperties properties { get; set; } + } + + + public class TwinPayloadProperties + { + public TwinProperties properties { get; set; } + } + public class TwinProperties { public Desired desired { get; set; } public Reported reported { get; set; } } - public class Desired { public int TimeToSleep { get; set; } + public Hashtable _metadata { get; set; } } public class Reported @@ -576,8 +705,13 @@ public class Reported public string Firmware { get; set; } public int TimeToSleep { get; set; } + + public Hashtable _metadata { get; set; } + + public int _version { get; set; } } + public class InvocationReceiveMessage { public int type { get; set; } diff --git a/nanoFramework.Json/JsonConvert.cs b/nanoFramework.Json/JsonConvert.cs index d8c1753..71ee90b 100644 --- a/nanoFramework.Json/JsonConvert.cs +++ b/nanoFramework.Json/JsonConvert.cs @@ -202,7 +202,15 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string var memberProperty = (JsonPropertyAttribute)m; - Debug.WriteLine($"{debugIndent} memberProperty.Name: {memberProperty?.Name ?? "null"} "); + Debug.WriteLine($"{debugIndent} memberProperty.Name: {memberProperty.Name ?? "null"} "); + + string memberPropertyName = memberProperty.Name; + + // workaround for for property names that start with '$' like Azure Twins + if (memberPropertyName[0] == '$') + { + memberPropertyName = "_" + memberProperty.Name.Substring(1); + } // Figure out if we're dealing with a Field or a Property and handle accordingly Type memberType = null; @@ -211,7 +219,7 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string MethodInfo memberPropGetMethod = null; bool memberIsProperty = false; - memberFieldInfo = rootType.GetField(memberProperty.Name); + memberFieldInfo = rootType.GetField(memberPropertyName); if (memberFieldInfo != null) { @@ -220,7 +228,7 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string } else { - memberPropGetMethod = rootType.GetMethod("get_" + memberProperty.Name); + memberPropGetMethod = rootType.GetMethod("get_" + memberPropertyName); if (memberPropGetMethod == null) { @@ -231,7 +239,7 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string else { memberType = memberPropGetMethod.ReturnType; - memberPropSetMethod = rootType.GetMethod("set_" + memberProperty.Name); + memberPropSetMethod = rootType.GetMethod("set_" + memberPropertyName); if (memberType == null) { @@ -241,7 +249,7 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string memberIsProperty = true; Debug.WriteLine($"{debugIndent} memberType: {memberType.Name} "); - Debug.WriteLine($"{debugIndent} memberPropGetMethod.Name: {memberPropGetMethod.Name} memberPropGetMethod.ReturnType: {memberPropGetMethod.ReturnType.Name}"); + Debug.WriteLine($"{debugIndent} memberPropGetMethod.Name: {memberPropertyName} memberPropGetMethod.ReturnType: {memberPropGetMethod.ReturnType.Name}"); } } @@ -255,11 +263,13 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string if (memberPath[memberPath.Length - 1] == '/') { - memberPath += memberProperty.Name; // Don't need to add a slash before appending rootElementType + // Don't need to add a slash before appending rootElementType + memberPath += memberPropertyName; } else { - memberPath = memberPath + '/' + memberProperty.Name; // Need to add a slash before appending rootElementType + // Need to add a slash before appending rootElementType + memberPath = memberPath + '/' + memberPropertyName; } object memberObject = null; @@ -271,12 +281,23 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string foreach (JsonPropertyAttribute v in ((JsonObjectAttribute)memberProperty.Value).Members) { - table.Add(v.Name, v.Value); + if (v.Value is JsonValue jsonValue) + { + table.Add(v.Name, (jsonValue).Value); + } + else if (v.Value is JsonObjectAttribute jsonObjectAttribute) + { + table.Add(v.Name, PopulateHashtable(jsonObjectAttribute)); + } + else if (v.Value is JsonArrayAttribute jsonArrayAttribute) + { + throw new NotImplementedException(); + } } memberObject = table; - Debug.WriteLine($"{debugIndent} populated the {memberProperty.Name} Hashtable"); + Debug.WriteLine($"{debugIndent} populated the {memberPropertyName} Hashtable"); } else { @@ -292,7 +313,7 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string memberFieldInfo.SetValue(rootInstance, memberObject); } - Debug.WriteLine($"{debugIndent} successfully initialized member {memberProperty.Name} to memberObject"); + Debug.WriteLine($"{debugIndent} successfully initialized member {memberPropertyName} to memberObject"); } else if (memberProperty.Value is JsonValue) { @@ -302,9 +323,11 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string if (memberType != typeof(DateTime)) { Debug.WriteLine($"{debugIndent} attempting to set rootInstance by invoking this member's set method for properties or SetValue() for fields"); + if (((JsonValue)memberProperty.Value).Value == null) { Debug.WriteLine($"{debugIndent} memberProperty.Value is null"); + if (memberIsProperty) { if (!memberPropGetMethod.ReturnType.IsValueType) @@ -319,9 +342,11 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string case "Single": memberPropSetMethod.Invoke(rootInstance, new object[] { Single.NaN }); break; + case "Double": memberPropSetMethod.Invoke(rootInstance, new object[] { Double.NaN }); break; + default: break; } @@ -333,29 +358,36 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string object obj = null; memberFieldInfo.SetValue(rootInstance, obj); } - Debug.WriteLine($"{debugIndent} successfully initialized member {memberProperty.Name} to null"); + Debug.WriteLine($"{debugIndent} successfully initialized member {memberPropertyName} to null"); } else { if (memberIsProperty) { JsonValue val = (JsonValue)memberProperty.Value; + Debug.WriteLine($"{debugIndent} setting value with memberPropSetMethod: {memberPropSetMethod.Name} Declaring Type: {memberPropSetMethod.DeclaringType} Value: {((JsonValue)memberProperty.Value).Value}"); + Debug.WriteLine($"{debugIndent} memberProperty.Value.Value.Type: {val.Value.GetType().Name} memberProperty.Value.Value: {val.Value}"); + if (val.Value.GetType() != memberType) { Debug.WriteLine($"{debugIndent} need to change memberProperty.Value.Value.Type to {memberType} to match memberPropGetMethod.ReturnType - why are these are different?!?"); + switch (memberType.Name) { case nameof(Int16): memberPropSetMethod.Invoke(rootInstance, new object[] { Convert.ToInt16(val.Value.ToString()) }); break; + case nameof(Byte): memberPropSetMethod.Invoke(rootInstance, new object[] { Convert.ToByte(val.Value.ToString()) }); break; + case nameof(Single): memberPropSetMethod.Invoke(rootInstance, new object[] { Convert.ToSingle(val.Value.ToString()) }); break; + default: memberPropSetMethod.Invoke(rootInstance, new object[] { ((JsonValue)memberProperty.Value).Value }); break; @@ -370,7 +402,8 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string { memberFieldInfo.SetValue(rootInstance, ((JsonValue)memberProperty.Value).Value); } - Debug.WriteLine($"{debugIndent} successfully initialized member {memberProperty.Name} to {((JsonValue)memberProperty.Value).Value} "); + + Debug.WriteLine($"{debugIndent} successfully initialized member {memberPropertyName} to {((JsonValue)memberProperty.Value).Value} "); } } else @@ -384,7 +417,7 @@ private static object PopulateObject(JsonToken rootToken, Type rootType, string memberFieldInfo.SetValue(rootInstance, ((JsonValue)memberProperty.Value).Value); } - Debug.WriteLine($"{debugIndent} successfully initialized member {memberProperty.Name} to {(JsonValue)memberProperty.Value} "); + Debug.WriteLine($"{debugIndent} successfully initialized member {memberPropertyName} to {(JsonValue)memberProperty.Value} "); } } @@ -769,12 +802,12 @@ private static ArrayList PopulateArrayList(JsonToken rootToken) mainTable.Add(memberProperty.Name, ((JsonValue)memberProperty.Value).Value); } - else if (memberProperty.Value is JsonArrayAttribute) + else if (memberProperty.Value is JsonArrayAttribute jsonArrayAttribute) { Debug.WriteLine($"{debugIndent} memberProperty.Value is a JArray"); // Create a JArray (memberValueArray) to hold the contents of memberProperty.Value - var memberValueArray = (JsonArrayAttribute)memberProperty.Value; + var memberValueArray = jsonArrayAttribute; // Create a temporary ArrayList memberValueArrayList - populate this as the memberItems are parsed var memberValueArrayList = new ArrayList(); @@ -786,11 +819,11 @@ private static ArrayList PopulateArrayList(JsonToken rootToken) foreach (JsonToken item in memberItems) { - if (item is JsonValue) + if (item is JsonValue jsonValue) { - memberValueArrayList.Add(((JsonValue)item).Value); + memberValueArrayList.Add((jsonValue).Value); } - else if (item is JsonToken) + else if (item is JsonToken jsonToken) { throw new NotImplementedException(); } @@ -824,6 +857,108 @@ private static ArrayList PopulateArrayList(JsonToken rootToken) return result; } + private static Hashtable PopulateHashtable(JsonToken rootToken) + { + var result = new Hashtable(); + + // Process all members for this rootObject + Debug.WriteLine($"{debugIndent} Entering rootObject.Members loop "); + + if (rootToken is JsonObjectAttribute rootTokenObjectAttribute) + { + foreach (var m in rootTokenObjectAttribute.Members) + { + Debug.WriteLine($"{debugIndent} Process rootObject.Member"); + + var memberProperty = (JsonPropertyAttribute)m; + + if (memberProperty == null) + { + Debug.WriteLine($"memberProperty is null and can't be"); + + throw new NotSupportedException(); + } + + Debug.WriteLine($"{debugIndent} memberProperty.Name: {memberProperty?.Name ?? "null"} "); + + // Process the member based on JObject, JValue, or JArray + if (memberProperty.Value is JsonObjectAttribute memberPropertyValue) + { + // Call PopulateObject() for this member - i.e. recursion + Debug.WriteLine($"{debugIndent} memberProperty.Value is JObject"); + + throw new NotImplementedException(); + + Debug.WriteLine($"{debugIndent} successfully initialized member {memberProperty.Name} to memberObject"); + } + else if (memberProperty.Value is JsonValue memberPropertyJsonValue) + { + if (memberPropertyJsonValue.Value is JsonValue jsonValue) + { + result.Add(memberProperty.Name, jsonValue.Value); + } + else if (memberPropertyJsonValue.Value is JsonObjectAttribute jsonObjectAttribute) + { + result.Add(memberProperty.Name, PopulateHashtable(jsonObjectAttribute)); + } + else if (memberProperty.Value is JsonArrayAttribute jsonArrayAttribute) + { + throw new NotImplementedException(); + } + } + else if (memberProperty.Value is JsonArrayAttribute jsonArrayAttribute) + { + Debug.WriteLine($"{debugIndent} memberProperty.Value is a JArray"); + + // Create a JArray (memberValueArray) to hold the contents of memberProperty.Value + var memberValueArray = jsonArrayAttribute; + + // Create a temporary ArrayList memberValueArrayList - populate this as the memberItems are parsed + var memberValueArrayList = new ArrayList(); + + // Create a JToken[] array for Items associated for this memberProperty.Value + JsonToken[] memberItems = memberValueArray.Items; + + //Debug.WriteLine($"{debugIndent} copy {memberItems.Length} memberItems from memberValueArray into memberValueArrayList - call PopulateObject() for items that aren't JValue"); + + foreach (JsonToken item in memberItems) + { + if (item is JsonValue jsonValue) + { + memberValueArrayList.Add(jsonValue); + } + else if (item is JsonToken jsonToken) + { + throw new NotImplementedException(); + } + else + { + Debug.WriteLine($"{debugIndent} item is not a JToken or a JValue - this case is not handled"); + } + } + + Debug.WriteLine($"{debugIndent} {memberItems.Length} memberValueArray.Items copied into memberValueArrayList - i.e. contents of memberProperty.Value"); + + // add to main table + result.Add(memberProperty.Name, memberValueArrayList); + + Debug.WriteLine($"{debugIndent} populated the rootInstance object with the contents of targetArray"); + } + } + } + else if (rootToken is JsonArrayAttribute) + { + throw new NotImplementedException(); + } + else + { + throw new NotImplementedException(); + } + + return result; + } + + // Trying to deserialize a stream in nanoFramework is problematic. // as Stream.Peek() has not been implemented in nanoFramework // Therefore, read all input into the static jsonBytes[] and use jsonPos to keep track of where we are when parsing the input @@ -1160,23 +1295,32 @@ private static LexToken GetNextTokenInternal() //Debug.Assert(ch == openQuote); var stringValue = sb.ToString(); - DateTime dtValue = DateTime.MinValue; + DateTime dtValue = DateTime.MaxValue; // check if this could be a DateTime value // min lenght is 18 for Java format: "Date(628318530718)": 18 if (stringValue.Length >= 18) { - try + // check for special case of "null" date + if(stringValue == "0001-01-01T00:00:00Z") { - dtValue = DateTimeExtensions.FromIso8601(stringValue); + dtValue = DateTime.MinValue; } - catch + + if (dtValue == DateTime.MaxValue) { - // intended, to catch failed conversion attempt + try + { + dtValue = DateTimeExtensions.FromIso8601(stringValue); + } + catch + { + // intended, to catch failed conversion attempt + } } - if (dtValue == DateTime.MinValue) + if (dtValue == DateTime.MaxValue) { try { @@ -1188,7 +1332,7 @@ private static LexToken GetNextTokenInternal() } } - if (dtValue == DateTime.MinValue) + if (dtValue == DateTime.MaxValue) { try { @@ -1200,7 +1344,7 @@ private static LexToken GetNextTokenInternal() } } - if (dtValue != DateTime.MinValue) + if (dtValue != DateTime.MaxValue) { return new LexToken() { TType = TokenType.Date, TValue = stringValue }; } diff --git a/nanoFramework.Json/JsonValue.cs b/nanoFramework.Json/JsonValue.cs index 09dcfc7..7ceb67a 100644 --- a/nanoFramework.Json/JsonValue.cs +++ b/nanoFramework.Json/JsonValue.cs @@ -18,18 +18,27 @@ public JsonValue(object value, bool isDateTime = false) { if (isDateTime) { - DateTime dtValue = DateTime.MinValue; - - try + DateTime dtValue = DateTime.MaxValue; + + // check for special case of "null" date + if ((string)value == "0001-01-01T00:00:00Z") { - dtValue = DateTimeExtensions.FromIso8601((string)value); + dtValue = DateTime.MinValue; } - catch + + if (dtValue == DateTime.MaxValue) { - // intended, to catch failed conversion attempt + try + { + dtValue = DateTimeExtensions.FromIso8601((string)value); + } + catch + { + // intended, to catch failed conversion attempt + } } - if (dtValue == DateTime.MinValue) + if (dtValue == DateTime.MaxValue) { try { @@ -41,7 +50,7 @@ public JsonValue(object value, bool isDateTime = false) } } - if (dtValue == DateTime.MinValue) + if (dtValue == DateTime.MaxValue) { try { @@ -53,7 +62,7 @@ public JsonValue(object value, bool isDateTime = false) } } - if (dtValue != DateTime.MinValue) + if (dtValue != DateTime.MaxValue) { Value = dtValue; } diff --git a/nanoFramework.Json/TimeExtensions.cs b/nanoFramework.Json/TimeExtensions.cs index 0b01720..51b89ec 100644 --- a/nanoFramework.Json/TimeExtensions.cs +++ b/nanoFramework.Json/TimeExtensions.cs @@ -100,7 +100,14 @@ public static DateTime FromIso8601(string date) string second = (parts.Length > 5) ? parts[5] : "0"; string ms = (parts.Length > 6) ? parts[6] : "0"; - DateTime dt = new DateTime(Convert.ToInt32(year), Convert.ToInt32(month), Convert.ToInt32(day), Convert.ToInt32(hour), Convert.ToInt32(minute), Convert.ToInt32(second), Convert.ToInt32(ms)); + // sanity check for bad milliseconds format + int milliseconds = Convert.ToInt32(ms); + if(milliseconds > 999) + { + milliseconds = 999; + } + + DateTime dt = new DateTime(Convert.ToInt32(year), Convert.ToInt32(month), Convert.ToInt32(day), Convert.ToInt32(hour), Convert.ToInt32(minute), Convert.ToInt32(second), milliseconds); if (utc) {