using System.Text.Json;
namespace AIStudio.Assistants.BeyondFactsLab;
public sealed partial class BeyondFactsLabStore
{
///
/// The folder of a preparation which holds a folder for each step.
///
private const string STEPS_DIRECTORY_NAME = "steps";
///
/// The file of a step which holds its form "Your details".
///
private const string FORM_FILE_NAME = "form.json";
///
/// Loads the form of a step.
///
/// The ID of the preparation.
/// The step.
/// The cancellation token.
/// The form, or an empty one when the person entered nothing yet.
/// This version cannot open the form.
public async Task LoadFormAsync(Guid projectId, BflStep step, CancellationToken token = default)
{
await this.InitializeAsync(token);
return await this.LoadFormWithoutInitializeAsync(projectId, step, token);
}
///
/// Loads the forms of every step of a preparation, e.g., to find missing details.
///
/// The ID of the preparation.
/// The cancellation token.
/// The forms this version can open, by their steps.
public async Task> LoadFormsAsync(Guid projectId, CancellationToken token = default)
{
await this.InitializeAsync(token);
var forms = new Dictionary();
foreach (var step in BflStep.All)
{
try
{
forms[step] = await this.LoadFormWithoutInitializeAsync(projectId, step, token);
}
catch (InvalidDataException exception)
{
logger.LogWarning(exception, "Could not open the form of the step '{Step}' of the preparation '{ProjectId}' of the BeyondFacts Lab.", step, projectId);
}
}
return forms;
}
///
/// Stores the form of a step and counts it as a change of the preparation.
///
/// The ID of the preparation.
/// The step.
/// The form.
/// The cancellation token.
/// The preparation with its new time of change.
/// The preparation does not exist.
/// This version cannot open the preparation or the stored form, so it does not write them either.
public async Task SaveFormAsync(Guid projectId, BflStep step, BflStepFormData form, CancellationToken token = default)
{
await this.InitializeAsync(token);
var gate = this.GetLock(projectId);
await gate.WaitAsync(token);
try
{
var project = await this.LoadRequiredWithoutInitializeAsync(projectId, token);
// A form which a newer version stored, or which is damaged, stays as it is:
await this.LoadFormWithoutInitializeAsync(projectId, step, token);
form.SchemaVersion = BflStepFormData.CURRENT_SCHEMA_VERSION;
await WriteTextAtomicAsync(this.FormPath(projectId, step), JsonSerializer.Serialize(form, JSON_OPTIONS), token);
project.ModifiedAtUtc = DateTimeOffset.UtcNow;
await this.StoreProjectAsync(project, token);
return project;
}
finally
{
gate.Release();
}
}
///
/// Reads the form of a step without the first start of the store.
///
/// The ID of the preparation.
/// The step.
/// The cancellation token.
/// The form, or an empty one when there is no file.
/// This version cannot open the form.
private async Task LoadFormWithoutInitializeAsync(Guid projectId, BflStep step, CancellationToken token)
{
var path = this.FormPath(projectId, step);
if (!File.Exists(path))
return new BflStepFormData();
BflStepFormData? form;
try
{
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 65_536, true);
form = await JsonSerializer.DeserializeAsync(stream, JSON_OPTIONS, token);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
{
throw new InvalidDataException("This version of AI Studio cannot open the form.", exception);
}
if (form is null || form.SchemaVersion is < 1 or > BflStepFormData.CURRENT_SCHEMA_VERSION || IsNull(form.Values))
throw new InvalidDataException("This version of AI Studio cannot open the form.");
return form;
}
///
/// Gets the file of the form of a step.
///
/// The ID of the preparation.
/// The step.
private string FormPath(Guid projectId, BflStep step) => Path.Combine(this.ProjectDirectory(projectId), STEPS_DIRECTORY_NAME, step.FolderName, FORM_FILE_NAME);
///
/// Checks for null a value which the type system says cannot be null, but which JSON can make null.
///
/// The value.
private static bool IsNull(T? value) => value is null;
}