Skip to content

Commit

Permalink
Make deserialization of flag override json files tolerant to comments…
Browse files Browse the repository at this point in the history
… and trailing commas in builds other than .NET 4.5 as well
  • Loading branch information
adams85 committed Nov 24, 2023
1 parent 45f3c6a commit 3c8f8b4
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 10 deletions.
63 changes: 63 additions & 0 deletions src/ConfigCat.Client.Tests/OverrideTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

#if USE_NEWTONSOFT_JSON
using JsonValue = Newtonsoft.Json.Linq.JValue;
Expand Down Expand Up @@ -473,6 +475,67 @@ public async Task LocalFile_Watcher_Reload_Sync()
File.Delete(SampleFileToCreate);
}

[TestMethod]
public async Task LocalFile_TolerantJsonParsing_SimplifiedConfig()
{
const string key = "flag";
const bool expectedEvaluatedValue = true;
var overrideValue = expectedEvaluatedValue.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();

var filePath = Path.GetTempFileName();
File.WriteAllText(filePath, $"{{ \"flags\": {{ \"{key}\": {overrideValue} }}, /* comment */ }}");

try
{
using var client = ConfigCatClient.Get("localhost", options =>
{
options.PollingMode = PollingModes.ManualPoll;
options.FlagOverrides = FlagOverrides.LocalFile(filePath, autoReload: false, OverrideBehaviour.LocalOnly);
});
var actualEvaluatedValue = await client.GetValueAsync<bool?>(key, null);

Assert.AreEqual(expectedEvaluatedValue, actualEvaluatedValue);
}
finally
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
}
}

[TestMethod]
public async Task LocalFile_TolerantJsonParsing_ComplexConfig()
{
const string key = "flag";
const bool expectedEvaluatedValue = true;
var overrideValue = expectedEvaluatedValue.ToString(CultureInfo.InvariantCulture).ToLowerInvariant();
var settingType = ((int)SettingType.Boolean).ToString(CultureInfo.InvariantCulture);

var filePath = Path.GetTempFileName();
File.WriteAllText(filePath, $"{{ \"f\": {{ \"{key}\": {{ \"t\": {settingType}, \"v\": {{ \"b\": {overrideValue} }} }} }}, /* comment */ }}");

try
{
using var client = ConfigCatClient.Get("localhost", options =>
{
options.PollingMode = PollingModes.ManualPoll;
options.FlagOverrides = FlagOverrides.LocalFile(filePath, autoReload: false, OverrideBehaviour.LocalOnly);
});
var actualEvaluatedValue = await client.GetValueAsync<bool?>(key, null);

Assert.AreEqual(expectedEvaluatedValue, actualEvaluatedValue);
}
finally
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
}
}

[DataRow(true, false, true)]
[DataRow(true, "", "")]
[DataRow(true, 0, 0)]
Expand Down
18 changes: 10 additions & 8 deletions src/ConfigCatClient/Extensions/SerializationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,36 @@ internal static class SerializationExtensions
#if USE_NEWTONSOFT_JSON
private static readonly JsonSerializer Serializer = JsonSerializer.Create();
#else
private static readonly JsonSerializerOptions SerializerOptions = new()
private static readonly JsonSerializerOptions TolerantSerializerOptions = new()
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
#endif

public static T? Deserialize<T>(this string json) => json.AsMemory().Deserialize<T>();
public static T? Deserialize<T>(this string json, bool tolerant = false) => json.AsMemory().Deserialize<T>(tolerant);

// NOTE: It would be better to use ReadOnlySpan<char>, however when the full string is wrapped in a span, json.ToString() result in a copy of the string.
// This is not the case with ReadOnlyMemory<char>, so we use that until support for .NET 4.5 support is dropped.
public static T? Deserialize<T>(this ReadOnlyMemory<char> json)
public static T? Deserialize<T>(this ReadOnlyMemory<char> json, bool tolerant = false)
{
#if USE_NEWTONSOFT_JSON
using var stringReader = new StringReader(json.ToString());
using var reader = new JsonTextReader(stringReader);
return Serializer.Deserialize<T>(reader);
#else
return JsonSerializer.Deserialize<T>(json.Span);
return JsonSerializer.Deserialize<T>(json.Span, tolerant ? TolerantSerializerOptions : null);
#endif
}

public static T? DeserializeOrDefault<T>(this string json) => json.AsMemory().DeserializeOrDefault<T>();
public static T? DeserializeOrDefault<T>(this string json, bool tolerant = false) => json.AsMemory().DeserializeOrDefault<T>(tolerant);

public static T? DeserializeOrDefault<T>(this ReadOnlyMemory<char> json)
public static T? DeserializeOrDefault<T>(this ReadOnlyMemory<char> json, bool tolerant = false)
{
try
{
return json.Deserialize<T>();
return json.Deserialize<T>(tolerant);
}
catch
{
Expand All @@ -54,7 +56,7 @@ public static string Serialize<T>(this T objectToSerialize)
#if USE_NEWTONSOFT_JSON
return JsonConvert.SerializeObject(objectToSerialize);
#else
return JsonSerializer.Serialize(objectToSerialize, SerializerOptions);
return JsonSerializer.Serialize(objectToSerialize, TolerantSerializerOptions);
#endif
}
}
4 changes: 2 additions & 2 deletions src/ConfigCatClient/Override/LocalFileDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,14 @@ private async Task ReloadFileAsync(bool isAsync, CancellationToken cancellationT
try
{
var content = File.ReadAllText(this.fullPath);
var simplified = content.DeserializeOrDefault<SimplifiedConfig>();
var simplified = content.DeserializeOrDefault<SimplifiedConfig>(tolerant: true);
if (simplified?.Entries is not null)
{
this.overrideValues = simplified.Entries.ToDictionary(kv => kv.Key, kv => kv.Value.ToSetting());
break;
}

var deserialized = content.Deserialize<Config>()
var deserialized = content.Deserialize<Config>(tolerant: true)
?? throw new InvalidOperationException("Invalid config JSON content: " + content);
this.overrideValues = deserialized.Settings;
break;
Expand Down

0 comments on commit 3c8f8b4

Please sign in to comment.