diff --git a/OpenNest.Data/NestDefaults.cs b/OpenNest.Data/NestDefaults.cs
index 66ae2ca..7fd24e0 100644
--- a/OpenNest.Data/NestDefaults.cs
+++ b/OpenNest.Data/NestDefaults.cs
@@ -73,9 +73,26 @@ public sealed class NestDefaults
/// present but unreadable/invalid, so callers can warn about a corrupt
/// file while still returning usable values.
///
- public static NestDefaults Load(string path, out NestDefaultsStatus status)
+ public static NestDefaults Load(string path, out NestDefaultsStatus status) =>
+ Load(path, Fallback.Units, out status);
+
+ ///
+ /// Loads defaults like ,
+ /// but a missing file, an unusable file, or a missing/undefined unit
+ /// field yields (the caller's existing
+ /// unit preference) instead of the built-in units. Other valid fields
+ /// still load. An undefined is ignored.
+ ///
+ public static NestDefaults Load(
+ string path,
+ Units fallbackUnits,
+ out NestDefaultsStatus status
+ )
{
var defaults = Fallback;
+ if (Enum.IsDefined(fallbackUnits))
+ defaults.Units = fallbackUnits;
+
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
status = NestDefaultsStatus.Missing;
@@ -104,10 +121,7 @@ public sealed class NestDefaults
status = NestDefaultsStatus.Ok;
- if (
- dto.Units is not null
- && Enum.TryParse(dto.Units, ignoreCase: true, out var units)
- )
+ if (TryParseUnits(dto.Units, out var units))
defaults.Units = units;
if (
@@ -192,7 +206,7 @@ public sealed class NestDefaults
var dto = new NestDefaultsDto
{
Version = CurrentVersion,
- Units = Units.ToString().ToLowerInvariant(),
+ Units = JsonSerializer.SerializeToElement(Units.ToString().ToLowerInvariant()),
Size = new SizeDto { Width = Size.Width, Length = Size.Length },
Quadrant = Quadrant,
PartSpacing = PartSpacing,
@@ -225,6 +239,30 @@ public sealed class NestDefaults
}
}
+ ///
+ /// Accepts only a defined unit name (case-insensitive). Enum.TryParse
+ /// would also accept numeric strings ("7") and comma-joined names,
+ /// producing undefined or unintended values.
+ ///
+ private static bool TryParseUnits(JsonElement? element, out Units units)
+ {
+ units = default;
+ if (element is not { ValueKind: JsonValueKind.String } value)
+ return false;
+
+ var text = value.GetString();
+ foreach (var candidate in Enum.GetValues())
+ {
+ if (string.Equals(candidate.ToString(), text, StringComparison.OrdinalIgnoreCase))
+ {
+ units = candidate;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private static bool IsUnreadableFile(Exception ex) =>
ex
is JsonException
@@ -252,7 +290,9 @@ public sealed class NestDefaults
private sealed record NestDefaultsDto
{
public int? Version { get; init; } = CurrentVersion;
- public string? Units { get; init; }
+ // Read as raw JSON so a wrongly typed unit falls back on its own
+ // instead of failing the whole file.
+ public JsonElement? Units { get; init; }
public SizeDto? Size { get; init; }
public int? Quadrant { get; init; }
public double? PartSpacing { get; init; }
diff --git a/OpenNest.Tests/Data/NestDefaultsTests.cs b/OpenNest.Tests/Data/NestDefaultsTests.cs
index f6b1004..6ff5cdf 100644
--- a/OpenNest.Tests/Data/NestDefaultsTests.cs
+++ b/OpenNest.Tests/Data/NestDefaultsTests.cs
@@ -147,6 +147,74 @@ public class NestDefaultsTests : IDisposable
Assert.Equal(1, loaded.PartSpacing);
}
+ [Theory]
+ [InlineData("\"7\"")]
+ [InlineData("\"1\"")]
+ [InlineData("\"-1\"")]
+ [InlineData("\"inches, millimeters\"")]
+ [InlineData("1")]
+ public void Load_UndefinedOrNumericUnits_FallBackPerField(string unitsJson)
+ {
+ File.WriteAllText(_path, $$"""{ "units": {{unitsJson}}, "partSpacing": 4 }""");
+
+ var loaded = NestDefaults.Load(_path, out var status);
+
+ // Only a defined unit name is accepted; the other fields still load.
+ Assert.Equal(NestDefaultsStatus.Ok, status);
+ Assert.Equal(Units.Inches, loaded.Units);
+ Assert.Equal(4, loaded.PartSpacing);
+ }
+
+ [Theory]
+ [InlineData("""{ "partSpacing": 4 }""")]
+ [InlineData("""{ "units": "7", "partSpacing": 4 }""")]
+ [InlineData("""{ "units": "furlongs", "partSpacing": 4 }""")]
+ [InlineData("""{ "units": null, "partSpacing": 4 }""")]
+ public void Load_MissingOrInvalidUnits_UseCallerFallbackUnits(string json)
+ {
+ File.WriteAllText(_path, json);
+
+ var loaded = NestDefaults.Load(_path, Units.Millimeters, out var status);
+
+ // The caller's existing unit preference survives a readable file
+ // that lacks a usable unit, and the valid fields still load.
+ Assert.Equal(NestDefaultsStatus.Ok, status);
+ Assert.Equal(Units.Millimeters, loaded.Units);
+ Assert.Equal(4, loaded.PartSpacing);
+ }
+
+ [Fact]
+ public void Load_SavedUnits_OverrideCallerFallbackUnits()
+ {
+ new NestDefaults { Units = Units.Inches }.Save(_path);
+
+ var loaded = NestDefaults.Load(_path, Units.Millimeters, out var status);
+
+ Assert.Equal(NestDefaultsStatus.Ok, status);
+ Assert.Equal(Units.Inches, loaded.Units);
+ }
+
+ [Fact]
+ public void Load_MissingOrCorruptFile_UsesCallerFallbackUnits()
+ {
+ var missing = NestDefaults.Load(_path, Units.Millimeters, out var missingStatus);
+ File.WriteAllText(_path, "{ this is not json");
+ var corrupt = NestDefaults.Load(_path, Units.Millimeters, out var corruptStatus);
+
+ Assert.Equal(NestDefaultsStatus.Missing, missingStatus);
+ Assert.Equal(Units.Millimeters, missing.Units);
+ Assert.Equal(NestDefaultsStatus.Invalid, corruptStatus);
+ Assert.Equal(Units.Millimeters, corrupt.Units);
+ }
+
+ [Fact]
+ public void Load_UndefinedCallerFallbackUnits_UseBuiltInUnits()
+ {
+ var loaded = NestDefaults.Load(_path, (Units)7, out _);
+
+ Assert.Equal(Units.Inches, loaded.Units);
+ }
+
[Fact]
public void Load_UnknownFieldsAndFutureVersion_Ignored()
{