Generated the JSON contracts of backups and the provenance store at compile time

This commit is contained in:
Thorsten Sommer committed 2026-11-01 21:11:40 +01:00
1 parent 620afd1611
commit 22b5d16c72
5 files changed
+58 -4

No files matched your search

@@ -51,12 +51,13 @@ public static class DataBackupFormat
/// How the manifest and the plan of a restore are written: readable for anybody who opens them,
/// and for the runtime, which applies the plan.
/// </summary>
public static readonly JsonSerializerOptions JSON_OPTIONS = new()
public static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
Converters = { new JsonStringEnumConverter() },
};
TypeInfoResolver = DataBackupJsonContext.Default,
}.AsReadOnly();
/// <summary>
/// File types which are compressed already. Compressing them once more costs time and makes the
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools.DataBackup;
/// <summary>
/// The contracts with which System.Text.Json reads and writes the manifest of a backup and the plan and report of
/// a restore, built at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(DataBackupManifest))]
[JsonSerializable(typeof(DataRestorePlan))]
[JsonSerializable(typeof(DataRestoreReport))]
internal sealed partial class DataBackupJsonContext : JsonSerializerContext;
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools.Databases.ProvenanceStore;
/// <summary>
/// The contracts with which System.Text.Json reads and writes the content around a passage, built at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(IReadOnlyList<string>))]
[JsonSerializable(typeof(List<string>))]
internal sealed partial class ProvenanceJsonContext : JsonSerializerContext;
@@ -39,7 +39,11 @@ public sealed class SqliteProvenanceStoreClientImplementation(string name, strin
/// Writes the content around a passage the way it reads: the default encoder would turn every
/// character outside ASCII into an escape, cf. ToolExecutionResult.MODEL_CONTENT_OPTIONS.
/// </summary>
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = ProvenanceJsonContext.Default,
}.AsReadOnly();
private readonly string databasePath = databasePath;
private readonly string lastCleanupPath = Path.Join(basePath, LAST_CLEANUP_FILENAME);
@@ -351,7 +355,7 @@ public sealed class SqliteProvenanceStoreClientImplementation(string name, strin
try
{
return JsonSerializer.Deserialize<List<string>>(row.SurroundingContent) ?? [];
return JsonSerializer.Deserialize<List<string>>(row.SurroundingContent, JSON_OPTIONS) ?? [];
}
catch (JsonException exception)
{
@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tests.Serialization;
/// <summary>
/// Checks that every contract AI Studio uses for its JSON comes from source generation, never from reflection.
/// </summary>
/// <remarks>
/// A type which no context of the options knows makes System.Text.Json throw, unless the options fall back to
/// reflection. Such a fallback hides the missing type until a trimmed build removes what reflection needs, and
/// only a published build shows it then.
/// </remarks>
[TestFixture]
public sealed class JsonCoverageTests
{
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
{
var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
.Where(type => root.Options.GetTypeInfo(type).OriginatingResolver is not JsonSerializerContext)
.Select(JsonContractDescription.TypeName);
Assert.That(typesWithoutGeneratedContract, Is.Empty);
}
}