feat(api): add NestResponse with SaveAsync/LoadAsync
Adds NestResponse type to OpenNest.Api with SaveAsync/LoadAsync for .nestquote format — a ZIP containing request.json, response.json (metrics), and an embedded nest.nest. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.IO;
|
||||||
|
|
||||||
|
namespace OpenNest.Api;
|
||||||
|
|
||||||
|
public class NestResponse
|
||||||
|
{
|
||||||
|
public int SheetCount { get; init; }
|
||||||
|
public double Utilization { get; init; }
|
||||||
|
public TimeSpan CutTime { get; init; }
|
||||||
|
public TimeSpan Elapsed { get; init; }
|
||||||
|
public Nest Nest { get; init; }
|
||||||
|
public NestRequest Request { get; init; }
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true,
|
||||||
|
IncludeFields = true // Required for OpenNest.Geometry.Size (public fields)
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task SaveAsync(string path)
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(path, FileMode.Create);
|
||||||
|
using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
|
||||||
|
|
||||||
|
// Write request.json
|
||||||
|
var requestEntry = zip.CreateEntry("request.json");
|
||||||
|
await using (var stream = requestEntry.Open())
|
||||||
|
{
|
||||||
|
await JsonSerializer.SerializeAsync(stream, Request, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write response.json (metrics only)
|
||||||
|
var metrics = new
|
||||||
|
{
|
||||||
|
SheetCount,
|
||||||
|
Utilization,
|
||||||
|
CutTimeTicks = CutTime.Ticks,
|
||||||
|
ElapsedTicks = Elapsed.Ticks
|
||||||
|
};
|
||||||
|
var responseEntry = zip.CreateEntry("response.json");
|
||||||
|
await using (var stream = responseEntry.Open())
|
||||||
|
{
|
||||||
|
await JsonSerializer.SerializeAsync(stream, metrics, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write embedded nest.nest via NestWriter → MemoryStream → ZIP entry
|
||||||
|
var nestEntry = zip.CreateEntry("nest.nest");
|
||||||
|
using var nestMs = new MemoryStream();
|
||||||
|
var writer = new NestWriter(Nest);
|
||||||
|
writer.Write(nestMs);
|
||||||
|
nestMs.Position = 0;
|
||||||
|
await using (var stream = nestEntry.Open())
|
||||||
|
{
|
||||||
|
await nestMs.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<NestResponse> LoadAsync(string path)
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
|
using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
|
||||||
|
|
||||||
|
// Read request.json
|
||||||
|
var requestEntry = zip.GetEntry("request.json");
|
||||||
|
NestRequest request;
|
||||||
|
await using (var stream = requestEntry!.Open())
|
||||||
|
{
|
||||||
|
request = await JsonSerializer.DeserializeAsync<NestRequest>(stream, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response.json
|
||||||
|
var responseEntry = zip.GetEntry("response.json");
|
||||||
|
JsonElement metricsJson;
|
||||||
|
await using (var stream = responseEntry!.Open())
|
||||||
|
{
|
||||||
|
metricsJson = await JsonSerializer.DeserializeAsync<JsonElement>(stream, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read embedded nest.nest via NestReader(Stream)
|
||||||
|
var nestEntry = zip.GetEntry("nest.nest");
|
||||||
|
Nest nest;
|
||||||
|
using (var nestMs = new MemoryStream())
|
||||||
|
{
|
||||||
|
await using (var stream = nestEntry!.Open())
|
||||||
|
{
|
||||||
|
await stream.CopyToAsync(nestMs);
|
||||||
|
}
|
||||||
|
nestMs.Position = 0;
|
||||||
|
var reader = new NestReader(nestMs);
|
||||||
|
nest = reader.Read();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NestResponse
|
||||||
|
{
|
||||||
|
SheetCount = metricsJson.GetProperty("sheetCount").GetInt32(),
|
||||||
|
Utilization = metricsJson.GetProperty("utilization").GetDouble(),
|
||||||
|
CutTime = TimeSpan.FromTicks(metricsJson.GetProperty("cutTimeTicks").GetInt64()),
|
||||||
|
Elapsed = TimeSpan.FromTicks(metricsJson.GetProperty("elapsedTicks").GetInt64()),
|
||||||
|
Nest = nest,
|
||||||
|
Request = request
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using OpenNest.Api;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
|
||||||
|
namespace OpenNest.Tests.Api;
|
||||||
|
|
||||||
|
public class NestResponsePersistenceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task SaveAsync_LoadAsync_RoundTrips()
|
||||||
|
{
|
||||||
|
var nest = new Nest("test-nest");
|
||||||
|
var plate = new Plate(new Size(60, 120));
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
|
||||||
|
var request = new NestRequest
|
||||||
|
{
|
||||||
|
Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }],
|
||||||
|
SheetSize = new Size(60, 120),
|
||||||
|
Material = "Steel",
|
||||||
|
Thickness = 0.125,
|
||||||
|
Spacing = 0.1
|
||||||
|
};
|
||||||
|
|
||||||
|
var original = new NestResponse
|
||||||
|
{
|
||||||
|
SheetCount = 1,
|
||||||
|
Utilization = 0.75,
|
||||||
|
CutTime = TimeSpan.FromMinutes(12.5),
|
||||||
|
Elapsed = TimeSpan.FromSeconds(3.2),
|
||||||
|
Nest = nest,
|
||||||
|
Request = request
|
||||||
|
};
|
||||||
|
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.nestquote");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await original.SaveAsync(path);
|
||||||
|
var loaded = await NestResponse.LoadAsync(path);
|
||||||
|
|
||||||
|
Assert.Equal(original.SheetCount, loaded.SheetCount);
|
||||||
|
Assert.Equal(original.Utilization, loaded.Utilization, precision: 4);
|
||||||
|
Assert.Equal(original.CutTime, loaded.CutTime);
|
||||||
|
Assert.Equal(original.Elapsed, loaded.Elapsed);
|
||||||
|
|
||||||
|
Assert.Equal(original.Request.Material, loaded.Request.Material);
|
||||||
|
Assert.Equal(original.Request.Thickness, loaded.Request.Thickness);
|
||||||
|
Assert.Equal(original.Request.Parts.Count, loaded.Request.Parts.Count);
|
||||||
|
Assert.Equal(original.Request.Parts[0].DxfPath, loaded.Request.Parts[0].DxfPath);
|
||||||
|
Assert.Equal(original.Request.Parts[0].Quantity, loaded.Request.Parts[0].Quantity);
|
||||||
|
|
||||||
|
Assert.NotNull(loaded.Nest);
|
||||||
|
Assert.Single(loaded.Nest.Plates);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user