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 @@
+