87 lines
5.4 KiB
C#
87 lines
5.4 KiB
C#
using System.Globalization;
|
|
using System.Text.Json;
|
|
|
|
namespace LehrerApp.Desktop.Services;
|
|
|
|
/// <summary>Verbrauch einer Aktion im Zeitraum, z. B. „Text korrigieren“ oder eine Planung der LehrerApp (alle Apps am selben Backend).</summary>
|
|
/// <param name="Operation">Funktion der Arbeitsblatt-API; null bei Aktionen anderer Apps.</param>
|
|
/// <param name="ChargedEur">Nur, wenn für jeden Aufruf der Euro-Gegenwert dokumentiert ist; sonst null (der Dienst schätzt nichts).</param>
|
|
public sealed record AiUsageGroup(string ActionId, string Label, string? Operation, int Calls, decimal ChargedUsd, decimal? ChargedEur);
|
|
|
|
/// <summary>Ein berechneter Aufruf. <see cref="At"/> ist die Uhrzeit der Datenbank des Dienstes, ohne Zeitzone.</summary>
|
|
public sealed record AiUsageEntry(DateTime At, string ActionId, string Label, string? Operation, string Status, decimal ChargedUsd, decimal? ChargedEur);
|
|
|
|
/// <param name="SpellcheckGranted">Ob die Rechtschreibprüfung über das Backend freigegeben ist; null, wenn der Dienst es nicht meldet.</param>
|
|
public sealed record AiUsageReport(int Days, DateTime From, DateTime To, int Calls, decimal ChargedUsd, decimal? ChargedEur,
|
|
IReadOnlyList<AiUsageGroup> ByAction, IReadOnlyList<AiUsageEntry> Entries, bool EntriesTruncated, bool? SpellcheckGranted)
|
|
{
|
|
/// <summary>
|
|
/// Liest die Antwort von <c>api/v1/usage.php</c>. Unbekannte zusätzliche Felder werden übergangen (der Dienst darf wachsen), fehlende oder
|
|
/// unplausible Pflichtfelder sind ein Fehler.
|
|
/// </summary>
|
|
public static AiUsageReport Parse(string json)
|
|
{
|
|
try
|
|
{
|
|
using var document = JsonDocument.Parse(json, new JsonDocumentOptions { MaxDepth = 16 });
|
|
var root = document.RootElement;
|
|
Need(root.GetProperty("apiVersion").GetString() == "1" && root.GetProperty("schema").GetString() == "lehrerbackend.usage.v1");
|
|
var period = root.GetProperty("period");
|
|
var days = period.GetProperty("days").GetInt32();
|
|
var totals = root.GetProperty("totals");
|
|
var groups = new List<AiUsageGroup>();
|
|
foreach (var item in root.GetProperty("byAction").EnumerateArray())
|
|
{
|
|
Need(groups.Count < 256);
|
|
groups.Add(new(Text(item, "actionId", 64), Text(item, "label", 200), Operation(item), item.GetProperty("calls").GetInt32(),
|
|
RequiredMoney(item, "chargedUsd"), Money(item, "chargedEur")));
|
|
}
|
|
var entries = new List<AiUsageEntry>();
|
|
foreach (var item in root.GetProperty("entries").EnumerateArray())
|
|
{
|
|
Need(entries.Count < 1000);
|
|
entries.Add(new(Date(item.GetProperty("at").GetString()), Text(item, "actionId", 64), Text(item, "label", 200), Operation(item), Text(item, "status", 32),
|
|
RequiredMoney(item, "chargedUsd"), Money(item, "chargedEur")));
|
|
}
|
|
bool? spellcheck = root.TryGetProperty("features", out var features) && features.TryGetProperty("spellcheck", out var flag) && flag.ValueKind is JsonValueKind.True or JsonValueKind.False
|
|
? flag.GetBoolean() : null;
|
|
var calls = totals.GetProperty("calls").GetInt32();
|
|
Need(days is >= 1 and <= 366 && calls >= 0 && groups.All(g => g.Calls >= 0 && g.ChargedUsd >= 0 && g.ChargedEur is null or >= 0) && entries.All(e => e.ChargedUsd >= 0));
|
|
return new(days, Date(period.GetProperty("from").GetString()), Date(period.GetProperty("to").GetString()), calls, RequiredMoney(totals, "chargedUsd"), Money(totals, "chargedEur"),
|
|
groups, entries, root.GetProperty("entriesTruncated").GetBoolean(), spellcheck);
|
|
}
|
|
catch (Exception e) when (e is JsonException or InvalidOperationException or KeyNotFoundException or FormatException or ArgumentException or OverflowException)
|
|
{ throw new AiBackendException("Die Verbrauchsdaten des KI-Dienstes konnten nicht verarbeitet werden."); }
|
|
}
|
|
|
|
private static void Need(bool condition) { if (!condition) throw new FormatException(); }
|
|
|
|
private static string Text(JsonElement item, string name, int max)
|
|
{
|
|
var value = item.GetProperty(name).GetString();
|
|
Need(!string.IsNullOrWhiteSpace(value) && value.Length <= max);
|
|
return value!;
|
|
}
|
|
|
|
private static string? Operation(JsonElement item) =>
|
|
item.TryGetProperty("operation", out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() : null;
|
|
|
|
private static decimal RequiredMoney(JsonElement item, string name) =>
|
|
Money(item, name) ?? throw new FormatException();
|
|
|
|
private static decimal? Money(JsonElement item, string name)
|
|
{
|
|
if (!item.TryGetProperty(name, out var value) || value.ValueKind == JsonValueKind.Null) return null;
|
|
// Dezimaltext mit höchstens sechs Nachkommastellen wie im ganzen Vertrag, nie eine Fließkommazahl
|
|
var text = value.GetString();
|
|
Need(text is not null && System.Text.RegularExpressions.Regex.IsMatch(text, @"^\d{1,9}(\.\d{1,6})?$"));
|
|
return decimal.Parse(text!, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private static DateTime Date(string? text)
|
|
{
|
|
Need(DateTime.TryParseExact(text, "yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out _));
|
|
return DateTime.ParseExact(text!, "yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture);
|
|
}
|
|
}
|