using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using OpenNest; using OpenNest.Data; namespace OpenNest.Server; /// /// Parses release requests with the drawing library's strictness: unknown or repeated query /// parameters and enum numbers or combinations are rejected, and server-computed or joined /// properties in a posted release are ignored, so a record read from the server may be sent back. /// internal static class ReleaseRequest { private static readonly string[] QueryKeys = { "setId", "nestId", "offset", "limit" }; // Integer enum values are refused; names are additionally checked before deserializing. private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false) }, }; public static bool TryParseQuery(IQueryCollection values, out Guid? setId, out Guid? nestId, out int offset, out int limit, out string error) { setId = null; nestId = null; offset = 0; limit = DrawingLibraryRules.DefaultRevisionLimit; foreach (var key in values.Keys) { if (!QueryKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) { error = $"Unknown query parameter '{key}'."; return false; } if (values[key].Count > 1) { error = $"Query parameter '{key}' was given more than once."; return false; } } if (!TryId(values, "setId", out setId, out error) || !TryId(values, "nestId", out nestId, out error) || !DrawingLibraryRequest.TryInteger(values, "offset", ref offset, out error) || !DrawingLibraryRequest.TryInteger(values, "limit", ref limit, out error)) return false; if (offset < 0 || limit < 1 || limit > DrawingLibraryRules.MaxRevisionLimit) { error = $"offset must be zero or more and limit between 1 and {DrawingLibraryRules.MaxRevisionLimit}."; return false; } return true; } /// /// Reads and validates one release JSON object. sets and nests must be /// present, enums must be single names, and joined or server-computed values are dropped. /// public static bool TryReadRelease(string? json, out SetReleaseRecord release, out string error) { release = new SetReleaseRecord(); if (string.IsNullOrWhiteSpace(json)) { error = "Missing release."; return false; } try { if (JsonNode.Parse(json) is not JsonObject root) { error = "The release must be a JSON object."; return false; } if (!DrawingSetRequest.RemoveJoined(root, "sets", "setName")) { error = "sets is required."; return false; } if (!DrawingSetRequest.RemoveJoined(root, "nests", "nest")) { error = "nests is required."; return false; } Remove(root, "releasedAt", "undoneAt", "undoneBy"); if (DrawingSetRequest.Property(root, "nests") is JsonArray nests) { for (var i = 0; i < nests.Count; i++) { if (nests[i] is not JsonObject nest) continue; Remove(nest, "undo", "undoRevision"); error = NameError(nest, "units", $"nests[{i}].units must be a supported unit name.") ?? NameError(nest, "cutType", $"nests[{i}].cutType must be Laser, Waterjet or Plasma.") ?? NameError(nest, "kind", $"nests[{i}].kind must be created or merged.") ?? ""; if (error.Length > 0) return false; } } release = root.Deserialize(Options) ?? release; } catch (JsonException) { error = "Invalid release JSON."; return false; } error = SetReleaseRules.GetError(release) ?? ""; return error.Length == 0; } public static bool TryReadUndo(string? json, out SetReleaseUndoRequest request, out string error) { request = new SetReleaseUndoRequest(); try { if (string.IsNullOrWhiteSpace(json) || JsonNode.Parse(json) is not JsonObject root) { error = "The undo request must be a JSON object."; return false; } request = root.Deserialize(Options) ?? request; } catch (JsonException) { error = "Invalid undo JSON."; return false; } error = SetReleaseRules.GetUndoError(request) ?? ""; return error.Length == 0; } private static bool TryId(IQueryCollection values, string key, out Guid? id, out string error) { id = null; error = ""; if (!values.TryGetValue(key, out var text)) return true; if (!Guid.TryParse(text.ToString(), out var parsed) || parsed == Guid.Empty) { error = $"{key} must be an id."; return false; } id = parsed; return true; } private static string? NameError(JsonObject node, string property, string error) where TEnum : struct, Enum => DrawingSetRequest.Property(node, property) is { } value && !DrawingSetRequest.IsName(value) ? error : null; private static void Remove(JsonObject node, params string[] names) { foreach (var key in node.Select(p => p.Key) .Where(k => names.Any(n => string.Equals(k, n, StringComparison.OrdinalIgnoreCase))).ToList()) node.Remove(key); } }