diff --git a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj index 56e376cc..6453f2da 100644 --- a/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj +++ b/OptimizelySDK.Net35/OptimizelySDK.Net35.csproj @@ -96,6 +96,9 @@ Entity\IdKeyEntity.cs + + + OptimizelyJson.cs Entity\TrafficAllocation.cs diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 4d669271..c261b579 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -98,6 +98,9 @@ Entity\IdKeyEntity.cs + + + OptimizelyJson.cs Entity\TrafficAllocation.cs diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index c51907f0..07eb88d8 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -31,6 +31,7 @@ + diff --git a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj index ed4ab047..c4634d9f 100644 --- a/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj +++ b/OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj @@ -114,6 +114,9 @@ Entity\IdKeyEntity.cs + + + Entity\OptimizelyJson.cs Entity\Rollout.cs diff --git a/OptimizelySDK.Tests/OptimizelyJsonTest.cs b/OptimizelySDK.Tests/OptimizelyJsonTest.cs new file mode 100644 index 00000000..f9b15503 --- /dev/null +++ b/OptimizelySDK.Tests/OptimizelyJsonTest.cs @@ -0,0 +1,255 @@ +/** + * + * Copyright 2020, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Moq; +using NUnit.Framework; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Exceptions; +using OptimizelySDK.Logger; +using System; +using System.Collections.Generic; + +namespace OptimizelySDK.Tests +{ + class ParentJson + { + public string strField { get; set; } + public int intField { get; set; } + public double doubleField { get; set; } + public bool boolField { get; set; } + public ObjectJson objectField { get; set; } + + } + class ObjectJson + { + public int inner_field_int { get; set; } + public double inner_field_double { get; set; } + public string inner_field_string {get;set;} + public bool inner_field_boolean { get; set; } + } + + class Field4 + { + public long inner_field1 { get; set; } + public InnerField2 inner_field2 { get; set; } + } + class InnerField2 : List { } + + + [TestFixture] + public class OptimizelyJsonTest + { + private string Payload; + private Dictionary Map; + private Mock LoggerMock; + private Mock ErrorHandlerMock; + + [SetUp] + public void Initialize() + { + ErrorHandlerMock = new Mock(); + ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); + + LoggerMock = new Mock(); + LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); + + Payload = "{ \"field1\": 1, \"field2\": 2.5, \"field3\": \"three\", \"field4\": {\"inner_field1\":3,\"inner_field2\":[\"1\",\"2\", 3, 4.23, true]}, \"field5\": true, }"; + Map = new Dictionary() { + { "strField", "john doe" }, + { "intField", 12 }, + { "doubleField", 2.23 }, + { "boolField", true}, + { "objectField", new Dictionary () { + { "inner_field_int", 3 }, + { "inner_field_double", 13.21 }, + { "inner_field_string", "john" }, + { "inner_field_boolean", true } + } + } + }; + } + + [Test] + public void TestOptimizelyJsonObjectIsValid() + { + var optimizelyJSONUsingMap = new OptimizelyJson(Map, ErrorHandlerMock.Object, LoggerMock.Object); + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.IsNotNull(optimizelyJSONUsingMap); + Assert.IsNotNull(optimizelyJSONUsingString); + } + [Test] + public void TestToStringReturnValidString() + { + var map = new Dictionary() { + { "strField", "john doe" }, + { "intField", 12 }, + { "objectField", new Dictionary () { + { "inner_field_int", 3 } + } + } + }; + var optimizelyJSONUsingMap = new OptimizelyJson(map, ErrorHandlerMock.Object, LoggerMock.Object); + string str = optimizelyJSONUsingMap.ToString(); + string expectedStringObj = "{\"strField\":\"john doe\",\"intField\":12,\"objectField\":{\"inner_field_int\":3}}"; + Assert.AreEqual(expectedStringObj, str); + } + + [Test] + public void TestGettingErrorUponInvalidJsonString() + { + var optimizelyJSONUsingString = new OptimizelyJson("{\"invalid\":}", ErrorHandlerMock.Object, LoggerMock.Object); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Provided string could not be converted to map."), Times.Once); + ErrorHandlerMock.Verify(er => er.HandleError(It.IsAny()), Times.Once); + } + + [Test] + public void TestOptimizelyJsonGetVariablesWhenSetUsingMap() + { + var optimizelyJSONUsingMap = new OptimizelyJson(Map, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.AreEqual(optimizelyJSONUsingMap.GetValue("strField"), "john doe"); + Assert.AreEqual(optimizelyJSONUsingMap.GetValue("intField"), 12); + Assert.AreEqual(optimizelyJSONUsingMap.GetValue("doubleField"), 2.23); + Assert.AreEqual(optimizelyJSONUsingMap.GetValue("boolField"), true); + Assert.AreEqual(optimizelyJSONUsingMap.GetValue("objectField.inner_field_int"), 3); + Assert.AreEqual(optimizelyJSONUsingMap.GetValue("objectField.inner_field_double"), 13.21); + Assert.AreEqual(optimizelyJSONUsingMap.GetValue("objectField.inner_field_string"), "john"); + Assert.AreEqual(optimizelyJSONUsingMap.GetValue("objectField.inner_field_boolean"), true); + Assert.IsTrue(optimizelyJSONUsingMap.GetValue>("objectField") is Dictionary); + } + + [Test] + public void TestOptimizelyJsonGetVariablesWhenSetUsingString() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + + Assert.AreEqual(optimizelyJSONUsingString.GetValue("field1"), 1); + Assert.AreEqual(optimizelyJSONUsingString.GetValue("field2"), 2.5); + Assert.AreEqual(optimizelyJSONUsingString.GetValue("field3"), "three"); + Assert.AreEqual(optimizelyJSONUsingString.GetValue("field4.inner_field1"), 3); + Assert.True(TestData.CompareObjects(optimizelyJSONUsingString.GetValue>("field4.inner_field2"), new List() { "1", "2", 3, 4.23, true })); + } + + [Test] + public void TestGetValueReturnsEntireDictWhenJsonPathIsEmptyAndTypeIsValid() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + var actualDict = optimizelyJSONUsingString.ToDictionary(); + var expectedValue = optimizelyJSONUsingString.GetValue>(""); + Assert.NotNull(expectedValue); + Assert.True(TestData.CompareObjects(expectedValue, actualDict)); + } + + [Test] + public void TestGetValueReturnsDefaultValueWhenJsonIsInvalid() + { + var payload = "{ \"field1\" : {1:\"Csharp\", 2:\"Java\"} }"; + var optimizelyJSONUsingString = new OptimizelyJson(payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue>("field1"); + // Even though above given JSON is not valid, newtonsoft is parsing it so + Assert.IsNotNull(expectedValue); + } + + [Test] + public void TestGetValueReturnsDefaultValueWhenTypeIsInvalid() + { + var payload = "{ \"field1\" : {\"1\":\"Csharp\",\"2\":\"Java\"} }"; + var optimizelyJSONUsingString = new OptimizelyJson(payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue>("field1"); + Assert.IsNotNull(expectedValue); + } + + [Test] + public void TestGetValueReturnsNullWhenJsonPathIsEmptyAndTypeIsOfObject() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue(""); + Assert.NotNull(expectedValue); + } + + [Test] + public void TestGetValueReturnsDefaultValueWhenJsonPathIsEmptyAndTypeIsNotValid() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue(""); + Assert.IsNull(expectedValue); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Value for path could not be assigned to provided type."), Times.Once); + ErrorHandlerMock.Verify(er => er.HandleError(It.IsAny()), Times.Once); + } + + [Test] + public void TestGetValueReturnsDefaultValueWhenJsonPathIsInvalid() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue("field11"); + Assert.IsNull(expectedValue); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Value for JSON key not found."), Times.Once); + ErrorHandlerMock.Verify(er => er.HandleError(It.IsAny()), Times.Once); + } + + [Test] + public void TestGetValueReturnsDefaultValueWhenJsonPath1IsInvalid() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue("field4."); + Assert.IsNull(expectedValue); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Value for JSON key not found."), Times.Once); + ErrorHandlerMock.Verify(er => er.HandleError(It.IsAny()), Times.Once); + } + + [Test] + public void TestGetValueReturnsDefaultValueWhenJsonPath2IsInvalid() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue("field4..inner_field1"); + Assert.IsNull(expectedValue); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Value for JSON key not found."), Times.Once); + ErrorHandlerMock.Verify(er => er.HandleError(It.IsAny()), Times.Once); + } + + [Test] + public void TestGetValueObjectNotModifiedIfCalledTwice() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue("field4.inner_field1"); + var expectedValue2 = optimizelyJSONUsingString.GetValue("field4.inner_field1"); + + Assert.AreEqual(expectedValue, expectedValue2); + } + + [Test] + public void TestGetValueReturnsUsingGivenClassType() + { + var optimizelyJSONUsingString = new OptimizelyJson(Payload, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJSONUsingString.GetValue("field4"); + + Assert.AreEqual(expectedValue.inner_field1, 3); + Assert.AreEqual(expectedValue.inner_field2, new List() { "1", "2", 3, 4.23, true }); + } + + [Test] + public void TestGetValueReturnsCastedObject() + { + var optimizelyJson = new OptimizelyJson(Map, ErrorHandlerMock.Object, LoggerMock.Object); + var expectedValue = optimizelyJson.ToDictionary(); + var actualValue = optimizelyJson.GetValue(null); + + Assert.IsTrue(TestData.CompareObjects(actualValue, expectedValue)); + } + } +} diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index c777eb91..786b8eaf 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -77,6 +77,7 @@ + diff --git a/OptimizelySDK/Exceptions/OptimizelyException.cs b/OptimizelySDK/Exceptions/OptimizelyException.cs index 56c7b3ed..5d9d4cc6 100644 --- a/OptimizelySDK/Exceptions/OptimizelyException.cs +++ b/OptimizelySDK/Exceptions/OptimizelyException.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017, Optimizely + * Copyright 2017, 2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,14 @@ public OptimizelyRuntimeException(string message) { } } + + public class InvalidJsonException : OptimizelyException + { + public InvalidJsonException(string message) + : base(message) + { + } + } public class InvalidAttributeException : OptimizelyException { diff --git a/OptimizelySDK/OptimizelyJson.cs b/OptimizelySDK/OptimizelyJson.cs new file mode 100644 index 00000000..398a075a --- /dev/null +++ b/OptimizelySDK/OptimizelyJson.cs @@ -0,0 +1,145 @@ +/* + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using OptimizelySDK.Logger; +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using OptimizelySDK.ErrorHandler; +using System.Linq; +using Newtonsoft.Json; + +namespace OptimizelySDK +{ + public class OptimizelyJson + { + private ILogger Logger; + private IErrorHandler ErrorHandler; + + private string Payload { get; set; } + private Dictionary Dict { get; set; } + + public OptimizelyJson(string payload, IErrorHandler errorHandler, ILogger logger) + { + try + { + ErrorHandler = errorHandler; + Logger = logger; + Dict = (Dictionary)ConvertIntoCollection(JObject.Parse(payload)); + Payload = payload; + } + catch (Exception exception) + { + logger.Log(LogLevel.ERROR, "Provided string could not be converted to map."); + ErrorHandler.HandleError(new Exceptions.InvalidJsonException(exception.Message)); + } + } + + public OptimizelyJson(Dictionary dict, IErrorHandler errorHandler, ILogger logger) + { + try + { + ErrorHandler = errorHandler; + Logger = logger; + Payload = JsonConvert.SerializeObject(dict); + Dict = dict; + } + catch (Exception exception) + { + logger.Log(LogLevel.ERROR, "Provided map could not be converted to string."); + ErrorHandler.HandleError(new Exceptions.InvalidJsonException(exception.Message)); + } + } + + override public string ToString() + { + return Payload; + } + + public Dictionary ToDictionary() + { + return Dict; + } + + /// + /// Returns the value from dictionary of given jsonPath (Seperated by ".") in the provided type T. + /// + /// Example: + /// If JSON Data is {"k1":true, "k2":{"k3":"v3"}} + /// + /// Set jsonPath to "k2" to access {"k3":"v3"} or set it to "k2.k3" to access "v3" + /// Set it to null or empty to access the entire JSON data but type must be Dictionary as generic type. + /// + /// Key path for the value. + /// Value if decoded successfully + public T GetValue(string jsonPath) + { + try + { + if (string.IsNullOrEmpty(jsonPath)) + { + return GetObject(Dict); + } + var path = jsonPath.Split('.'); + + var currentObject = Dict; + for (int i = 0; i < path.Length - 1; i++) + { + currentObject = currentObject[path[i]] as Dictionary; + } + return GetObject(currentObject[path[path.Length - 1]]); + } + catch (KeyNotFoundException exception) + { + Logger.Log(LogLevel.ERROR, "Value for JSON key not found."); + ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); + } + catch (Exception exception) + { + Logger.Log(LogLevel.ERROR, "Value for path could not be assigned to provided type."); + ErrorHandler.HandleError(new Exceptions.OptimizelyRuntimeException(exception.Message)); + } + return default(T); + } + + private T GetObject(object o) + { + if (!(o is T deserializedObj)) + { + deserializedObj = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(o)); + } + return deserializedObj; + } + + /// + /// This will convert all the given JObjects datatype variables into Dictionaries and JArray objects into List. + /// + /// object containing JObject and JArray datatype objects + /// Dictionary object + private object ConvertIntoCollection(object o) + { + if (o is JObject jo) + { + return jo.ToObject>().ToDictionary(k => k.Key, v => ConvertIntoCollection(v.Value)); + } + else if (o is JArray ja) + { + return ja.ToObject>().Select(ConvertIntoCollection).ToList(); + } + return o; + } + } +} diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index 01ff091b..239bf02a 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -86,6 +86,7 @@ +