using System.Text.Json;
using System.Text.Json.Nodes;
using OpenNest.Data;
namespace OpenNest.Server;
///
/// Parses drawing set requests with the drawing library's strictness: unknown, repeated
/// or out-of-range values are rejected rather than ignored or clamped, and sort accepts
/// names only.
///
internal static class DrawingSetRequest
{
private static readonly string[] QueryKeys =
{
"search", "customer", "includeArchived", "sort", "order", "offset", "limit",
};
private static readonly JsonSerializerOptions SetOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
};
public static bool TryParseQuery(IQueryCollection values, out DrawingSetQuery query, out string error)
{
query = new DrawingSetQuery();
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;
}
}
var sort = DrawingSetSortField.SavedAt;
if (values.TryGetValue("sort", out var sortText) && !DrawingLibraryRequest.TryEnumName(sortText.ToString(), out sort))
{
error = $"sort must be one of: {string.Join(", ", Enum.GetNames().Select(DrawingLibraryRequest.CamelCase))}.";
return false;
}
var descending = true;
if (values.TryGetValue("order", out var orderText))
{
if (string.Equals(orderText, "asc", StringComparison.OrdinalIgnoreCase))
{
descending = false;
}
else if (!string.Equals(orderText, "desc", StringComparison.OrdinalIgnoreCase))
{
error = "order must be asc or desc.";
return false;
}
}
var includeArchived = false;
if (values.TryGetValue("includeArchived", out var archivedText) && !bool.TryParse(archivedText.ToString(), out includeArchived))
{
error = "includeArchived must be true or false.";
return false;
}
var offset = 0;
var limit = DrawingSetQuery.DefaultLimit;
if (!DrawingLibraryRequest.TryInteger(values, "offset", ref offset, out error)
|| !DrawingLibraryRequest.TryInteger(values, "limit", ref limit, out error))
return false;
query = new DrawingSetQuery
{
Search = values["search"].ToString(),
Customer = values["customer"].ToString(),
IncludeArchived = includeArchived,
Sort = sort,
Descending = descending,
Offset = offset,
Limit = limit,
};
error = query.GetValidationError() ?? "";
return error.Length == 0;
}
///
/// Reads and validates one set JSON object. members must be present; each
/// member's joined drawing is ignored, so a record read from the server may be
/// sent back as it is.
///
public static bool TryReadSet(string? json, out DrawingSetRecord record, out string error)
{
record = new DrawingSetRecord();
if (string.IsNullOrWhiteSpace(json))
{
error = "Missing set.";
return false;
}
try
{
if (JsonNode.Parse(json) is not JsonObject root)
{
error = "The set must be a JSON object.";
return false;
}
if (!RemoveJoined(root, "members", "drawing"))
{
error = "members is required.";
return false;
}
record = root.Deserialize(SetOptions) ?? record;
}
catch (JsonException)
{
error = "Invalid set JSON.";
return false;
}
error = DrawingSetRules.GetError(record) ?? "";
return error.Length == 0;
}
internal static JsonNode? Property(JsonObject node, string name) =>
node.FirstOrDefault(p => string.Equals(p.Key, name, StringComparison.OrdinalIgnoreCase)).Value;
internal static bool IsName(JsonNode value)
where TEnum : struct, Enum =>
value is JsonValue text && text.TryGetValue(out var name) && DrawingLibraryRequest.TryEnumName(name, out _);
// Drops the server-joined property from each object in the list, so a record read from
// the server may be sent back; false when the list property is missing altogether.
internal static bool RemoveJoined(JsonObject root, string list, string joined)
{
if (!root.Any(p => string.Equals(p.Key, list, StringComparison.OrdinalIgnoreCase)))
return false;
if (Property(root, list) is JsonArray items)
{
foreach (var item in items.OfType())
{
foreach (var key in item.Select(p => p.Key)
.Where(k => string.Equals(k, joined, StringComparison.OrdinalIgnoreCase)).ToList())
item.Remove(key);
}
}
return true;
}
}