Generated the JSON contracts of the runtime calls at compile time

This commit is contained in:
Thorsten Sommer committed 2026-11-02 19:14:16 +01:00
1 parent c97b0f0f76
commit 25fcabb549
8 files changed
+177 -22

No files matched your search

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools.Services;
/// <summary>
/// The contracts with which System.Text.Json reads the events of a file extraction from the runtime, built at
/// compile time.
/// </summary>
/// <remarks>
/// A converter reads the metadata of an event as the type its JSON tells, so each of these types needs a
/// contract of its own here. A test fails for one which is missing.
/// </remarks>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(ContentStreamSseEvent))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(ContentStreamTextMetadata))]
[JsonSerializable(typeof(ContentStreamPdfMetadata))]
[JsonSerializable(typeof(ContentStreamSpreadsheetMetadata))]
[JsonSerializable(typeof(ContentStreamPresentationMetadata))]
[JsonSerializable(typeof(ContentStreamImageMetadata))]
[JsonSerializable(typeof(ContentStreamDocumentMetadata))]
[JsonSerializable(typeof(ContentStreamErrorMetadata))]
[JsonSerializable(typeof(ContentStreamPromptInjectionMetadata))]
internal sealed partial class RustContentStreamJsonContext : JsonSerializerContext;
@@ -0,0 +1,89 @@
using System.Text.Json.Serialization;
using AIStudio.Tools.Databases.VectorStore;
using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Services;
/// <summary>
/// The contracts with which System.Text.Json reads and writes nearly every call to the runtime, built at
/// compile time.
/// </summary>
/// <remarks>
/// The calls to the vector store are generic: every query of its client needs the answer type with the type
/// of its result, e.g., DatabaseQueryResponse&lt;bool?&gt;. A test fails for a request or an answer which is
/// missing here.
/// </remarks>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
// The requests:
[JsonSerializable(typeof(SelectSecretRequest))]
[JsonSerializable(typeof(StoreSecretRequest))]
[JsonSerializable(typeof(RustService.ShareFileRequest))]
[JsonSerializable(typeof(PreviousDirectory))]
[JsonSerializable(typeof(SelectFileOptions))]
[JsonSerializable(typeof(SaveFileOptions))]
[JsonSerializable(typeof(OpenPathRequest))]
[JsonSerializable(typeof(OpenDocumentRequest))]
[JsonSerializable(typeof(RegisterShortcutRequest))]
[JsonSerializable(typeof(ValidateShortcutRequest))]
[JsonSerializable(typeof(SetInterfaceSizeRequest))]
[JsonSerializable(typeof(CreateMediaJobRequest))]
[JsonSerializable(typeof(LogEventRequest))]
[JsonSerializable(typeof(ValidateTokenizerRequest))]
[JsonSerializable(typeof(StoreTokenizerRequest))]
[JsonSerializable(typeof(DeleteTokenizerRequest))]
[JsonSerializable(typeof(CountTokensRequest))]
[JsonSerializable(typeof(PrepareImageRequest))]
[JsonSerializable(typeof(PrepareImageForModelRequest))]
// The answers:
[JsonSerializable(typeof(RequestedSecret))]
[JsonSerializable(typeof(StoreSecretResponse))]
[JsonSerializable(typeof(DeleteSecretResponse))]
[JsonSerializable(typeof(ShareFileResponse))]
[JsonSerializable(typeof(ImagePrepareResponse))]
[JsonSerializable(typeof(PreparedModelImage))]
[JsonSerializable(typeof(TauriEvent))]
[JsonSerializable(typeof(UpdateResponse))]
[JsonSerializable(typeof(SetClipboardResponse))]
[JsonSerializable(typeof(DirectorySelectionResponse))]
[JsonSerializable(typeof(FileSelectionResponse))]
[JsonSerializable(typeof(FilesSelectionResponse))]
[JsonSerializable(typeof(FileSaveResponse))]
[JsonSerializable(typeof(OpenPathResponse))]
[JsonSerializable(typeof(OpenDocumentResponse))]
[JsonSerializable(typeof(ShortcutRegistrationResult))]
[JsonSerializable(typeof(ShortcutValidationResponse))]
[JsonSerializable(typeof(ShortcutResponse))]
[JsonSerializable(typeof(RuntimeInfoResponse))]
[JsonSerializable(typeof(AppExitResponse))]
[JsonSerializable(typeof(AppRestartResponse))]
[JsonSerializable(typeof(InterfaceSizeResponse))]
[JsonSerializable(typeof(CreateMediaJobResponse))]
[JsonSerializable(typeof(MediaJobEvent))]
[JsonSerializable(typeof(List<EnterpriseConfig>))]
[JsonSerializable(typeof(GetLogPathsResponse))]
[JsonSerializable(typeof(TokenizerResponse))]
// The vector store, with the result of every query its client sends:
[JsonSerializable(typeof(QdrantEdgeInfo))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.EnsureVectorStoreRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.CheckVectorStoreRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.InsertEmbeddingRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.SearchEmbeddingRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.RetrieveVectorsRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.DeleteEmbeddingByFileRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.RelocateEmbeddingFileRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.OptimizeVectorStoreRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.DeleteVectorStoreRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.PrepareBackupRequest))]
[JsonSerializable(typeof(QdrantEdgeClientImplementation.CopyStoreForBackupRequest))]
[JsonSerializable(typeof(RustService.DatabaseOperationResponse))]
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<VectorStoreEnsureResult>))]
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<bool?>))]
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<List<VectorSearchResult>>))]
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<List<StoredVector>>))]
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<VectorStoreBackupPreparation>))]
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<long?>))]
internal sealed partial class RustJsonContext : JsonSerializerContext;
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Services;
/// <summary>
/// The contracts with which System.Text.Json reads and writes the calls to the prompt injection filter of the
/// runtime, built at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(SanitizePromptInjectionsRequest))]
[JsonSerializable(typeof(SanitizePromptInjectionsResponse))]
[JsonSerializable(typeof(SanitizePromptInjectionsBatchRequest))]
[JsonSerializable(typeof(SanitizePromptInjectionsBatchResponse))]
internal sealed partial class RustPromptInjectionJsonContext : JsonSerializerContext;
@@ -112,7 +112,7 @@ public sealed partial class RustService
try
{
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
if (sseEvent is null)
continue;
@@ -364,7 +364,7 @@ public sealed partial class RustService
ContentStreamSseEvent? sseEvent = null;
try
{
sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
}
catch (JsonException)
{
@@ -460,7 +460,7 @@ public sealed partial class RustService
{
try
{
var errorMessage = JsonSerializer.Deserialize<string>(jsonContent);
var errorMessage = JsonSerializer.Deserialize<string>(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
if (string.IsNullOrWhiteSpace(errorMessage))
return false;
@@ -40,7 +40,8 @@ public sealed partial class RustService
using var response = await this.http.PostAsJsonAsync(
"/security/prompt-injection/sanitize",
new SanitizePromptInjectionsRequest(text),
cancellationToken: timeoutTokenSource.Token);
PROMPT_INJECTION_JSON_OPTIONS,
timeoutTokenSource.Token);
if (!response.IsSuccessStatusCode)
{
@@ -48,7 +49,7 @@ public sealed partial class RustService
return null;
}
return await response.Content.ReadFromJsonAsync<SanitizePromptInjectionsResponse>(timeoutTokenSource.Token);
return await response.Content.ReadFromJsonAsync<SanitizePromptInjectionsResponse>(PROMPT_INJECTION_JSON_OPTIONS, timeoutTokenSource.Token);
}
catch (Exception exception)
{
@@ -82,7 +83,8 @@ public sealed partial class RustService
using var response = await this.http.PostAsJsonAsync(
"/security/prompt-injection/sanitize-batch",
new SanitizePromptInjectionsBatchRequest(texts),
cancellationToken: timeoutTokenSource.Token);
PROMPT_INJECTION_JSON_OPTIONS,
timeoutTokenSource.Token);
if (!response.IsSuccessStatusCode)
{
@@ -90,7 +92,7 @@ public sealed partial class RustService
return null;
}
var batchResponse = await response.Content.ReadFromJsonAsync<SanitizePromptInjectionsBatchResponse>(timeoutTokenSource.Token);
var batchResponse = await response.Content.ReadFromJsonAsync<SanitizePromptInjectionsBatchResponse>(PROMPT_INJECTION_JSON_OPTIONS, timeoutTokenSource.Token);
if (batchResponse.Results is null || batchResponse.Results.Count != texts.Count)
{
this.logger?.LogError("The prompt injection filter answered with {ResultCount} result(s) for {TextCount} text(s).", batchResponse.Results?.Count ?? 0, texts.Count);
@@ -37,15 +37,38 @@ public sealed partial class RustService : BackgroundService
/// <summary>
/// The JSON options of nearly every call to the runtime, which names its fields in snake case.
/// </summary>
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
Converters =
{
new RustEnumConverter(),
},
};
TypeInfoResolver = RustJsonContext.Default,
}.AsReadOnly();
/// <summary>
/// The JSON options of the events of a file extraction, which name their fields themselves.
/// </summary>
/// <remarks>
/// The same as the defaults of System.Text.Json, with which these events were read before.
/// </remarks>
internal static readonly JsonSerializerOptions CONTENT_STREAM_JSON_OPTIONS = new JsonSerializerOptions
{
TypeInfoResolver = RustContentStreamJsonContext.Default,
}.AsReadOnly();
/// <summary>
/// The JSON options of the prompt injection filter, which names its fields in camel case.
/// </summary>
/// <remarks>
/// The same as the defaults of the JSON calls of HttpClient, with which the filter was called before.
/// </remarks>
internal static readonly JsonSerializerOptions PROMPT_INJECTION_JSON_OPTIONS = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
TypeInfoResolver = RustPromptInjectionJsonContext.Default,
}.AsReadOnly();
private ILogger<RustService>? logger;
private Encryption? encryptor;
private string? cachedUserLanguage;
@@ -16,6 +16,8 @@ public sealed class JsonCoverageTests
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderRequests))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeRequests))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeAnswers))]
public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
{
var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
@@ -1,5 +1,4 @@
using System.Reflection;
using System.Text.Json;
using AIStudio.Tools;
using AIStudio.Tools.Databases.VectorStore;
@@ -42,9 +41,9 @@ public static partial class JsonTestRoots
foreach (var type in typeof(QdrantEdgeClientImplementation).GetNestedTypes(BindingFlags.NonPublic).Where(type => type.Name.EndsWith("Request", StringComparison.Ordinal)).OrderBy(type => type.Name, StringComparer.Ordinal))
yield return Case(new(type, options));
// The prompt injection filter, which gets called with the defaults of HttpClient, i.e., with camel case:
yield return Case(new(typeof(SanitizePromptInjectionsRequest), JsonSerializerOptions.Web));
yield return Case(new(typeof(SanitizePromptInjectionsBatchRequest), JsonSerializerOptions.Web));
// The prompt injection filter, which names its fields in camel case:
yield return Case(new(typeof(SanitizePromptInjectionsRequest), RustService.PROMPT_INJECTION_JSON_OPTIONS));
yield return Case(new(typeof(SanitizePromptInjectionsBatchRequest), RustService.PROMPT_INJECTION_JSON_OPTIONS));
}
/// <summary>
@@ -94,16 +93,16 @@ public static partial class JsonTestRoots
yield return Case(new(typeof(RustService.DatabaseQueryResponse<long?>), options));
//
// The events of a file extraction, which get read with the defaults of System.Text.Json. A converter reads
// their metadata as the type the JSON tells:
// The events of a file extraction, which name their fields themselves. A converter reads their metadata as
// the type the JSON tells:
//
yield return Case(new(typeof(ContentStreamSseEvent), JsonSerializerOptions.Default));
yield return Case(new(typeof(string), JsonSerializerOptions.Default), "error of a file extraction");
yield return Case(new(typeof(ContentStreamSseEvent), RustService.CONTENT_STREAM_JSON_OPTIONS));
yield return Case(new(typeof(string), RustService.CONTENT_STREAM_JSON_OPTIONS), "error of a file extraction");
foreach (var type in ImplementationsOf(typeof(ContentStreamSseMetadata)))
yield return Case(new(type, JsonSerializerOptions.Default));
yield return Case(new(type, RustService.CONTENT_STREAM_JSON_OPTIONS));
// The prompt injection filter, which gets called with the defaults of HttpClient, i.e., with camel case:
yield return Case(new(typeof(SanitizePromptInjectionsResponse), JsonSerializerOptions.Web));
yield return Case(new(typeof(SanitizePromptInjectionsBatchResponse), JsonSerializerOptions.Web));
// The prompt injection filter, which names its fields in camel case:
yield return Case(new(typeof(SanitizePromptInjectionsResponse), RustService.PROMPT_INJECTION_JSON_OPTIONS));
yield return Case(new(typeof(SanitizePromptInjectionsBatchResponse), RustService.PROMPT_INJECTION_JSON_OPTIONS));
}
}