Files
OpenNest/OpenNest.Data/DrawingLibraryRules.cs
T
ajandClaude Opus 5.5 76133193df feat(server): drawing library schema and routes
The server becomes the system of record for shared drawings (cut type,
thickness, material and grade, customer, notes). Schema v4 adds `drawings`
(current row) and `drawing_revisions` (immutable) to the same SQLite file under
the existing monitor; the nest tables are untouched and a v3 upgrade keeps every
nest row byte-identical.

`/api/drawings` follows the nest revision contract: client-allocated ids,
If-Match on every existing-drawing write (428/400/409 with no state change),
archive instead of delete, and one transaction for promotion plus snapshot.
Geometry is an opaque package (a zero-plate .nest) the server never parses.
Enum fields accept single names only, because the JSON enum converter would
otherwise accept numbers and comma combinations. Material, grade and customer
filters match SharedListNames keys so spacing and case cannot split a drawing
family; thickness is stored and compared at four decimals.

Shared bounds live in OpenNest.Data so the client can refuse the same requests
before sending. If-Match parsing moves to one helper used by nests and drawings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:33:02 -04:00

74 lines
3.2 KiB
C#

using OpenNest;
namespace OpenNest.Data;
/// <summary>
/// Bounds shared by the drawing library client and server. The client checks them
/// before sending; the server refuses anything outside them with no state change.
/// </summary>
public static class DrawingLibraryRules
{
/// <summary>Longest name, customer, material, grade or saved-by text.</summary>
public const int MaxTextLength = 200;
public const int MaxNotesLength = 4000;
/// <summary>Largest accepted drawing package (16 MiB).</summary>
public const int MaxPackageBytes = 16 * 1024 * 1024;
public const string PackageTooLargeError = "The drawing package exceeds 16 MiB.";
public const int DefaultRevisionLimit = 100;
public const int MaxRevisionLimit = 200;
/// <summary>Thickness as stored and compared: rounded to four decimals.</summary>
public static double NormalizeThickness(double thickness) =>
System.Math.Round(thickness, 4, MidpointRounding.AwayFromZero);
/// <summary>The first metadata rule <paramref name="record"/> breaks, or null when it may be saved.</summary>
public static string? GetMetadataError(DrawingLibraryRecord? record)
{
if (record is null)
return "Missing metadata.";
if (string.IsNullOrWhiteSpace(record.Name))
return "name is required.";
return TextError("name", record.Name, MaxTextLength)
?? TextError("customer", record.Customer, MaxTextLength)
?? TextError("materialName", record.MaterialName, MaxTextLength)
?? TextError("materialGrade", record.MaterialGrade, MaxTextLength)
?? TextError("savedBy", record.SavedBy, MaxTextLength)
?? TextError("notes", record.Notes, MaxNotesLength)
?? (!double.IsFinite(record.Thickness) || record.Thickness < 0
? "thickness must be a finite number, zero or greater."
: null)
?? (!Enum.IsDefined(record.Units) ? "units must be a supported unit name." : null)
?? (!Enum.IsDefined(record.CutType) ? "cutType must be Laser, Waterjet or Plasma." : null);
}
/// <summary>The first package rule <paramref name="package"/> breaks, or null when it may be stored.</summary>
public static string? GetPackageError(ReadOnlySpan<byte> package)
{
if (package.Length == 0)
return "The drawing package is empty.";
if (package.Length > MaxPackageBytes)
return PackageTooLargeError;
// Every ZIP archive, including an empty one, starts with a "PK" record signature.
if (package.Length < 4 || package[0] != (byte)'P' || package[1] != (byte)'K')
return "The drawing package is not a ZIP archive.";
return null;
}
internal static string? TextError(string field, string? value, int maxLength)
{
if (value is null)
return $"{field} is required.";
if (value.Length > maxLength)
return $"{field} must be at most {maxLength} characters.";
// SQLite LIKE stops at an embedded NUL; refuse it everywhere for one rule.
if (value.Contains('\0'))
return $"{field} must not contain NUL characters.";
return null;
}
}