< Summary

Information
Class: Elsa.Platform.Integration.Services.FileShellConfigurationOverlayStore
Assembly: Elsa.Platform.Integration
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Platform.Integration/Services/FileShellConfigurationOverlayStore.cs
Line coverage
90%
Covered lines: 66
Uncovered lines: 7
Coverable lines: 73
Total lines: 148
Line coverage: 90.4%
Branch coverage
68%
Covered branches: 33
Total branches: 48
Branch coverage: 68.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
ConfigureFeaturesAsync()75%1616100%
ConfigureSettingsAsync()100%44100%
LoadAsync()16.66%9657.14%
SaveIfChangedAsync()75%4491.66%
GetPath()50%22100%
EnsureObject(...)100%44100%
ToNode(...)50%4480%
Merge(...)75%10866.66%
Serialize(...)100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Platform.Integration/Services/FileShellConfigurationOverlayStore.cs

#LineLine coverage
 1using System.Text.Json;
 2using System.Text.Json.Nodes;
 3using Elsa.Platform.Integration.Options;
 4using Microsoft.Extensions.Hosting;
 5using Microsoft.Extensions.Options;
 6
 7namespace Elsa.Platform.Integration.Services;
 8
 29public sealed class FileShellConfigurationOverlayStore(
 210    IOptions<ElsaPlatformIntegrationOptions> options,
 211    IHostEnvironment environment) : IShellConfigurationOverlayStore
 12{
 113    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
 114    {
 115        WriteIndented = true
 116    };
 217    private readonly SemaphoreSlim _semaphore = new(1, 1);
 18
 19    public async Task<bool> ConfigureFeaturesAsync(
 20        string shellId,
 21        IReadOnlyDictionary<string, JsonElement>? enabledFeatures,
 22        IReadOnlyList<string>? disabledFeatures,
 23        CancellationToken cancellationToken = default)
 24    {
 125        await _semaphore.WaitAsync(cancellationToken);
 26        try
 27        {
 128            var root = await LoadAsync(cancellationToken);
 129            var before = Serialize(root);
 130            var features = EnsureObject(root, "CShells", "Shells", shellId, "Features");
 31
 432            foreach (var feature in enabledFeatures ?? new Dictionary<string, JsonElement>())
 133                features[feature.Key] = ToNode(feature.Value) ?? new JsonObject();
 34
 435            foreach (var featureId in disabledFeatures ?? [])
 136                features.Remove(featureId);
 37
 138            var state = EnsureObject(root, "Elsa", "PlatformIntegration", "Shells", shellId);
 139            var disabledFeatureArray = new JsonArray();
 440            foreach (var featureId in disabledFeatures ?? [])
 141                disabledFeatureArray.Add(featureId);
 142            state["DisabledFeatures"] = disabledFeatureArray;
 43
 144            return await SaveIfChangedAsync(root, before, cancellationToken);
 45        }
 46        finally
 47        {
 148            _semaphore.Release();
 49        }
 150    }
 51
 52    public async Task<bool> ConfigureSettingsAsync(
 53        string shellId,
 54        JsonElement settings,
 55        CancellationToken cancellationToken = default)
 56    {
 157        await _semaphore.WaitAsync(cancellationToken);
 58        try
 59        {
 160            var root = await LoadAsync(cancellationToken);
 161            var before = Serialize(root);
 162            var configuration = EnsureObject(root, "CShells", "Shells", shellId, "Configuration");
 163            if (ToNode(settings) is JsonObject settingsObject)
 164                Merge(configuration, settingsObject);
 65
 166            return await SaveIfChangedAsync(root, before, cancellationToken);
 67        }
 68        finally
 69        {
 170            _semaphore.Release();
 71        }
 172    }
 73
 74    private async Task<JsonObject> LoadAsync(CancellationToken cancellationToken)
 75    {
 276        var path = GetPath();
 277        if (!File.Exists(path))
 278            return new JsonObject();
 79
 080        await using var stream = File.OpenRead(path);
 081        return await JsonNode.ParseAsync(stream, cancellationToken: cancellationToken) as JsonObject
 082            ?? throw new InvalidOperationException("The Platform shell overlay file must contain a JSON object.");
 283    }
 84
 85    private async Task<bool> SaveIfChangedAsync(JsonObject root, string before, CancellationToken cancellationToken)
 86    {
 287        var after = Serialize(root);
 288        if (string.Equals(before, after, StringComparison.Ordinal))
 089            return false;
 90
 291        var path = GetPath();
 292        var directory = Path.GetDirectoryName(path);
 293        if (!string.IsNullOrWhiteSpace(directory))
 294            Directory.CreateDirectory(directory);
 95
 296        var tempPath = $"{path}.{Guid.NewGuid():N}.tmp";
 297        await File.WriteAllTextAsync(tempPath, after, cancellationToken);
 298        File.Move(tempPath, path, overwrite: true);
 299        return true;
 2100    }
 101
 102    private string GetPath()
 103    {
 4104        var path = options.Value.ShellOverlayPath;
 4105        return Path.IsPathRooted(path) ? path : Path.Combine(environment.ContentRootPath, path);
 106    }
 107
 108    private static JsonObject EnsureObject(JsonObject root, params string[] path)
 109    {
 3110        JsonObject current = root;
 30111        foreach (var segment in path)
 112        {
 12113            if (current[segment] is not JsonObject child)
 114            {
 12115                child = new JsonObject();
 12116                current[segment] = child;
 117            }
 118
 12119            current = child;
 120        }
 121
 3122        return current;
 123    }
 124
 125    private static JsonNode? ToNode(JsonElement element) =>
 2126        element.ValueKind switch
 2127        {
 0128            JsonValueKind.Undefined or JsonValueKind.Null => null,
 2129            _ => JsonNode.Parse(element.GetRawText())
 2130        };
 131
 132    private static void Merge(JsonObject target, JsonObject source)
 133    {
 4134        foreach (var property in source)
 135        {
 1136            if (property.Value is JsonObject sourceObject && target[property.Key] is JsonObject targetObject)
 137            {
 0138                Merge(targetObject, sourceObject);
 0139                continue;
 140            }
 141
 1142            target[property.Key] = property.Value?.DeepClone();
 143        }
 1144    }
 145
 146    private static string Serialize(JsonObject root) =>
 4147        root.ToJsonString(JsonOptions);
 148}