diff --git a/OpenTabletDriver.Tests/ConfigurationTest.cs b/OpenTabletDriver.Tests/ConfigurationTest.cs
index 610bca38c..b6f557509 100644
--- a/OpenTabletDriver.Tests/ConfigurationTest.cs
+++ b/OpenTabletDriver.Tests/ConfigurationTest.cs
@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Text;
using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
using Newtonsoft.Json.Schema.Generation;
@@ -300,6 +303,85 @@ public void Configurations_Verify_Configs_With_Schema()
Assert.False(failed);
}
+ ///
+ /// Ensures that configuration formatting/linting matches expectations, which are:
+ /// - 2 space indentation
+ /// - Newline at end of file
+ /// - Consistent newline format
+ ///
+ [Fact]
+ public void Configurations_Are_Linted()
+ {
+ const int maxLinesToOutput = 3;
+
+ var serializer = new JsonSerializer();
+ var failedFiles = 0;
+
+ var ourJsonSb = new StringBuilder();
+ using var strw = new StringWriter(ourJsonSb);
+ using var jtw = new JsonTextWriter(strw);
+ jtw.Formatting = Formatting.Indented;
+ jtw.Indentation = 2;
+
+ foreach (var (tabletFilename, theirJson) in ConfigFiles)
+ {
+ ourJsonSb.Clear();
+ var ourJsonObj = JsonConvert.DeserializeObject(theirJson);
+
+ serializer.Serialize(jtw, ourJsonObj);
+ ourJsonSb.AppendLine(); // otherwise we won't have an EOL at EOF
+
+ var ourJson = ourJsonSb.ToString();
+
+ var failedLines = DoesJsonMatch(ourJson, theirJson);
+
+ if (failedLines.Any() || !string.Equals(theirJson, ourJson)) // second check ensures EOL markers are equivalent
+ {
+ failedFiles++;
+ _testOutputHelper.WriteLine(
+ $"- Tablet Configuration '{tabletFilename}' lint check failed with the following errors:");
+
+ foreach (var (line, error) in failedLines.Take(maxLinesToOutput))
+ _testOutputHelper.WriteLine($" Line {line}: {error}");
+ if (failedLines.Count > maxLinesToOutput)
+ _testOutputHelper.WriteLine($" Truncated an additional {failedLines.Count - maxLinesToOutput} mismatching lines - wrong indent?");
+ else if (failedLines.Count == 0)
+ _testOutputHelper.WriteLine(" Generic mismatch (line endings?)");
+ }
+ }
+
+ Assert.Equal(0, failedFiles);
+ }
+
+ private static IList<(int, string)> DoesJsonMatch(string ourJson, string theirJson)
+ {
+ int line = 0;
+ var rv = new List<(int, string)>();
+
+ using var ourSr = new StringReader(ourJson);
+ using var theirSr = new StringReader(theirJson);
+ while (true)
+ {
+ var ourLine = ourSr.ReadLine();
+ var theirLine = theirSr.ReadLine();
+ line++;
+
+ if (ourLine == null && theirLine == null)
+ break; // success for file
+
+ var ourLineOutput = ourLine ?? "EOF";
+ var theirLineOutput = theirLine ?? "EOF";
+
+ if (ourLine == null || theirLine == null || !string.Equals(ourLine, theirLine))
+ rv.Add((line, $"Expected '{ourLineOutput}' got '{theirLineOutput}'"));
+
+ if (ourLine == null || theirLine == null)
+ break;
+ }
+
+ return rv;
+ }
+
private static void DisallowAdditionalItemsAndProperties(JSchema schema)
{
schema.AllowAdditionalItems = false;
diff --git a/OpenTabletDriver/Tablet/DigitizerSpecifications.cs b/OpenTabletDriver/Tablet/DigitizerSpecifications.cs
index 74ddc6146..bdf171caa 100644
--- a/OpenTabletDriver/Tablet/DigitizerSpecifications.cs
+++ b/OpenTabletDriver/Tablet/DigitizerSpecifications.cs
@@ -1,6 +1,9 @@
+using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
+using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
+using Newtonsoft.Json;
namespace OpenTabletDriver.Tablet
{
@@ -14,18 +17,21 @@ public class DigitizerSpecifications
/// The width of the digitizer in millimeters.
///
[Required(ErrorMessage = $"Digitizer ${nameof(Width)} must be defined")]
+ [JsonConverter(typeof(DecimalJsonConverter))]
public float Width { set; get; }
///
/// The height of the digitizer in millimeters.
///
[Required(ErrorMessage = $"Digitizer ${nameof(Height)} must be defined")]
+ [JsonConverter(typeof(DecimalJsonConverter))]
public float Height { set; get; }
///
/// The maximum X coordinate for the digitizer.
///
[Required(ErrorMessage = $"Digitizer ${nameof(MaxX)} must be defined")]
+ [JsonConverter(typeof(DecimalJsonConverter))]
[DisplayName("Max X")]
public float MaxX { set; get; }
@@ -33,7 +39,43 @@ public class DigitizerSpecifications
/// The maximum Y coordinate for the digitizer.
///
[Required(ErrorMessage = $"Digitizer ${nameof(MaxY)} must be defined")]
+ [JsonConverter(typeof(DecimalJsonConverter))]
[DisplayName("Max Y")]
public float MaxY { set; get; }
}
+
+ public class DecimalJsonConverter : JsonConverter
+ {
+ public override bool CanRead => false;
+ public override bool CanWrite => true;
+
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(double);
+ }
+
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
+ {
+ if (value == null)
+ throw new ArgumentNullException();
+
+ writer.WriteRawValue(IsWholeValue(value)
+ ? JsonConvert.ToString(Convert.ToInt64(value))
+ : JsonConvert.ToString(value));
+ }
+
+ public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+
+ [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator")]
+ private static bool IsWholeValue(object value)
+ {
+ if (value is float floatValue)
+ return floatValue == Math.Truncate(floatValue);
+
+ return false;
+ }
+ }
}