< Summary

Class:Willykc.Templ.Editor.Entry.TemplEntryCore
Assembly:Willykc.Templ.Editor
File(s):/github/workspace/Packages/package.to.test/Editor/Entry/TemplEntryCore.cs
Covered lines:242
Uncovered lines:0
Coverable lines:242
Total lines:459
Line coverage:100% (242 of 242)
Covered branches:0
Total branches:0
Covered methods:25
Total methods:25
Method coverage:100% (25 of 25)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
TemplEntryCore(...)0%990100%
OnAssetsChanged(...)0%440100%
OnAfterAssemblyReload()0%440100%
OnWillDeleteAsset(...)0%440100%
FlagChangedEntry(...)0%330100%
RenderAllValidEntries()0%330100%
RenderEntry(...)0%440100%
HandleRequestToRemoveLiveEntries(...)0%220100%
GetEagerDeferredEntries()0%440100%
IsPathReferencedByEntry(...)0%110100%
FunctionConflictsDetected()0%330100%
IsSettingsChanged(...)0%110100%
RenderChangedEntries()0%220100%
FlagDeferredEntries(...)0%330100%
FlagInputDeletedEntries(...)0%330100%
RenderEagerEntries(...)0%110100%
RenderEntries(...)0%330100%
RenderEntriesWithProgress(...)0%330100%
RenderEntry(...)0%330100%
CheckOverrides(...)0%440100%
GetContext(...)0%220100%
IsMatchPath(...)0%220100%
GetAssetDirectoryPath(...)0%110100%
GetEntryWithIndex(...)0%110100%
DecomposeChanges(...)0%990100%

File(s)

/github/workspace/Packages/package.to.test/Editor/Entry/TemplEntryCore.cs

#LineLine coverage
 1/*
 2 * Copyright (c) 2024 Willy Alberto Kuster
 3 *
 4 * Permission is hereby granted, free of charge, to any person obtaining a copy
 5 * of this software and associated documentation files (the "Software"), to deal
 6 * in the Software without restriction, including without limitation the rights
 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 8 * copies of the Software, and to permit persons to whom the Software is
 9 * furnished to do so, subject to the following conditions:
 10 *
 11 * The above copyright notice and this permission notice shall be included in
 12 * all copies or substantial portions of the Software.
 13 *
 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 20 * THE SOFTWARE.
 21 */
 22using Scriban;
 23using Scriban.Runtime;
 24using System;
 25using System.Collections.Generic;
 26using System.IO;
 27using System.Linq;
 28
 29namespace Willykc.Templ.Editor.Entry
 30{
 31    using Abstraction;
 32    using static TemplSettings;
 33
 34    internal sealed class TemplEntryCore : ITemplEntryCore
 35    {
 36        private const string LiveEntriesTitle = "Templ Live Entries";
 37        private const string ProgressBarRenderingInfo = "Rendering...";
 38        private const string TemplChangedKey = "templ.changed";
 39        private const string TemplDeferredKey = "templ.deferred";
 40        private const string OkDialogText = "Remove Live Entries ({0})";
 41        private const string CancelDialogText = "Abort";
 42        private const string DialogMessage = "The '{0}' asset/directory currently referenced by " +
 43            LiveEntriesTitle + " is about to be deleted. Press 'Remove Live Entries' to delete " +
 44            "the corresponding " + LiveEntriesTitle + ". Press '" + CancelDialogText + "' to " +
 45            "cancel the removal of both the asset and the " + LiveEntriesTitle;
 46
 47        private const char AssetPathSeparator = '/';
 48
 49        private readonly IAssetDatabase assetDatabase;
 50        private readonly IFileSystem fileSystem;
 51        private readonly ISessionState sessionState;
 52        private readonly ILogger log;
 53        private readonly ISettingsProvider settingsProvider;
 54        private readonly IEditorUtility editorUtility;
 55        private readonly List<Type> functions;
 56        private readonly string[] functionConflicts;
 57        private readonly string[] functionNames;
 58
 7359        internal TemplEntryCore(
 60            IAssetDatabase assetDatabase,
 61            IFileSystem fileSystem,
 62            ISessionState sessionState,
 63            ILogger log,
 64            ISettingsProvider settingsProvider,
 65            ITemplateFunctionProvider templateFunctionProvider,
 66            IEditorUtility editorUtility)
 7367        {
 7368            this.assetDatabase = assetDatabase ??
 69                throw new ArgumentNullException(nameof(assetDatabase));
 7370            this.fileSystem = fileSystem ??
 71                throw new ArgumentNullException(nameof(fileSystem));
 7372            this.sessionState = sessionState ??
 73                throw new ArgumentNullException(nameof(sessionState));
 7374            this.log = log ??
 75                throw new ArgumentNullException(nameof(log));
 7376            this.settingsProvider = settingsProvider ??
 77                throw new ArgumentNullException(nameof(settingsProvider));
 7378            this.editorUtility = editorUtility ??
 79                throw new ArgumentNullException(nameof(editorUtility));
 7380            templateFunctionProvider = templateFunctionProvider ??
 81                throw new ArgumentNullException(nameof(templateFunctionProvider));
 82
 7383            functions = templateFunctionProvider.GetTemplateFunctionTypes().ToList();
 7384            functionConflicts = templateFunctionProvider.GetDuplicateTemplateFunctionNames();
 7385            functionNames = templateFunctionProvider.GetTemplateFunctionNames();
 86
 7387            if (functionConflicts.Length > 0)
 1288            {
 1289                log.Error("Function name conflicts detected: " +
 90                    string.Join(", ", functionConflicts));
 1291            }
 7392        }
 93
 94        void ITemplEntryCore.OnAssetsChanged(AssetsPaths changes)
 15995        {
 15996            if (!settingsProvider.SettingsExist() || FunctionConflictsDetected())
 14497            {
 14498                return;
 99            }
 100
 15101            if (IsSettingsChanged(changes))
 2102            {
 2103                RenderChangedEntries();
 2104                return;
 105            }
 106
 13107            var eagerDeferred = GetEagerDeferredEntries();
 13108            var entriesToRender = settingsProvider.GetSettings().ValidEntries
 304109                .Where(e => DecomposeChanges(changes, e).Any(c => e.ShouldRender(c)))
 110                .Union(eagerDeferred)
 111                .Distinct();
 112
 13113            FlagDeferredEntries(entriesToRender, changes);
 13114            RenderEagerEntries(entriesToRender, changes);
 159115        }
 116
 117        void ITemplEntryCore.OnAfterAssemblyReload()
 6118        {
 6119            if (!settingsProvider.SettingsExist() || FunctionConflictsDetected())
 3120            {
 3121                return;
 122            }
 123
 3124            var deferred = sessionState.GetString(TemplDeferredKey);
 125
 3126            if (string.IsNullOrWhiteSpace(deferred))
 1127            {
 1128                return;
 129            }
 130
 2131            var deferredEntries = settingsProvider.GetSettings().ValidEntries
 10132                .Where(e => deferred.Contains(e.Id));
 2133            RenderEntries(deferredEntries);
 2134            sessionState.EraseString(TemplDeferredKey);
 6135        }
 136
 137        bool ITemplEntryCore.OnWillDeleteAsset(string path)
 85138        {
 85139            if (!settingsProvider.SettingsExist() || FunctionConflictsDetected())
 73140            {
 73141                return true;
 142            }
 143
 12144            var settings = settingsProvider.GetSettings();
 145
 12146            var referencedEntries = settings.Entries
 24147                .Where(e => IsPathReferencedByEntry(e, path))
 148                .ToList();
 149
 12150            if (referencedEntries.Count > 0)
 8151            {
 8152                var agreedToRemoveEntries = HandleRequestToRemoveLiveEntries(path, settings,
 153                    referencedEntries);
 8154                return agreedToRemoveEntries;
 155            }
 156
 4157            FlagInputDeletedEntries(path);
 4158            return true;
 85159        }
 160
 161        void ITemplEntryCore.FlagChangedEntry(TemplEntry entry)
 4162        {
 4163            if (entry == null)
 1164            {
 1165                return;
 166            }
 167
 3168            var existing = sessionState.GetString(TemplChangedKey);
 169
 3170            if (!existing.Contains(entry.Id))
 2171            {
 2172                sessionState.SetString(TemplChangedKey, existing + entry.Id);
 2173            }
 4174        }
 175
 176        void ITemplEntryCore.RenderAllValidEntries()
 15177        {
 15178            if (!settingsProvider.SettingsExist() || FunctionConflictsDetected())
 3179            {
 3180                return;
 181            }
 182
 12183            RenderEntries(settingsProvider.GetSettings().ValidEntries);
 15184        }
 185
 186        void ITemplEntryCore.RenderEntry(string id)
 12187        {
 12188            if (!settingsProvider.SettingsExist() || FunctionConflictsDetected())
 3189            {
 3190                return;
 191            }
 192
 20193            var entry = settingsProvider.GetSettings().ValidEntries.FirstOrDefault(e => e.Id == id);
 194
 9195            if (entry == null)
 4196            {
 4197                log.Error($"Could not find valid entry with id '{id}'");
 4198                return;
 199            }
 200
 5201            RenderEntries(new[] { entry });
 12202        }
 203
 204        private bool HandleRequestToRemoveLiveEntries(string path, TemplSettings settings,
 205            List<TemplEntry> referencedEntries)
 8206        {
 8207            var message = string.Format(DialogMessage, path);
 8208            var okText = string.Format(OkDialogText, referencedEntries.Count);
 8209            var agreedToRemoveEntries = editorUtility.DisplayDialog(LiveEntriesTitle, message,
 210                okText, CancelDialogText);
 211
 8212            if (agreedToRemoveEntries)
 4213            {
 12214                referencedEntries.ForEach(e => settings.Entries.Remove(e));
 215
 4216                editorUtility.SetDirty(settings);
 4217                assetDatabase.SaveAssets();
 4218            }
 219
 8220            return agreedToRemoveEntries;
 8221        }
 222
 223        private IList<TemplEntry> GetEagerDeferredEntries()
 13224        {
 13225            var deferred = sessionState.GetString(TemplDeferredKey);
 226
 13227            if (string.IsNullOrEmpty(deferred))
 9228            {
 9229                return EmptyEntryArray;
 230            }
 231
 4232            var eagerDeferred = settingsProvider.GetSettings().ValidEntries
 8233                .Where(e => !e.Deferred && deferred.Contains(e.Id))
 234                .ToList();
 4235            deferred = eagerDeferred
 2236                .Select(e => e.Id)
 2237                .Aggregate(deferred, (result, next) => result.Replace(next, string.Empty));
 4238            sessionState.SetString(TemplDeferredKey, deferred);
 4239            return eagerDeferred;
 13240        }
 241
 24242        private bool IsPathReferencedByEntry(TemplEntry entry, string path) => (new[]
 243        {
 244            assetDatabase.GetAssetPath(entry.InputAsset),
 245            assetDatabase.GetAssetPath(entry.Directory),
 246            assetDatabase.GetAssetPath(entry.Template)
 52247        }).Any(p => !string.IsNullOrEmpty(p) && IsMatchPath(path, p));
 248
 249        private bool FunctionConflictsDetected()
 61250        {
 61251            if (functionConflicts.Length > 0 || functionNames.Contains(NameOfOutputAssetPath))
 10252            {
 10253                log.Error("Templates will not render due to function name conflicts");
 10254                return true;
 255            }
 256
 51257            return false;
 61258        }
 259
 260        private bool IsSettingsChanged(AssetsPaths changes)
 15261        {
 15262            var settingsPath = assetDatabase.GetAssetPath(settingsProvider.GetSettings());
 15263            return changes.importedAssets.Contains(settingsPath);
 15264        }
 265
 266        private void RenderChangedEntries()
 2267        {
 2268            var changed = sessionState.GetString(TemplChangedKey);
 269
 2270            if (string.IsNullOrWhiteSpace(changed))
 1271            {
 1272                return;
 273            }
 274
 1275            var entriesToRender = settingsProvider.GetSettings().ValidEntries
 5276                .Where(e => changed.Contains(e.Id));
 1277            RenderEntries(entriesToRender);
 1278            sessionState.EraseString(TemplChangedKey);
 2279        }
 280
 281        private void FlagDeferredEntries(IEnumerable<TemplEntry> entries, AssetsPaths changes)
 13282        {
 13283            var deferred = entries
 13284                .Where(e => e.Deferred &&
 8285                DecomposeChanges(changes, e).All(c => !e.IsTemplateChanged(c)))
 2286                .Select(e => e.Id);
 13287            var deferredFlags = string.Concat(deferred);
 288
 13289            if (!string.IsNullOrWhiteSpace(deferredFlags))
 2290            {
 2291                sessionState.SetString(TemplDeferredKey, deferredFlags);
 2292            }
 13293        }
 294
 295        private void FlagInputDeletedEntries(string path)
 4296        {
 4297            var deferred = settingsProvider.GetSettings().ValidEntries
 8298                .Where(e => e.ShouldRender(new AssetChange(ChangeType.Delete, path)))
 1299                .Select(e => e.Id);
 4300            var deferredFlags = string.Concat(deferred);
 301
 4302            if (!string.IsNullOrWhiteSpace(deferredFlags))
 1303            {
 1304                sessionState.SetString(TemplDeferredKey, deferredFlags);
 1305            }
 4306        }
 307
 308        private void RenderEagerEntries(
 309            IEnumerable<TemplEntry> entries,
 310            AssetsPaths changes)
 13311        {
 13312            var entriesToRenderNow = entries
 37313                .Where(e => !e.Deferred ||
 24314                DecomposeChanges(changes, e).Any(c => e.IsTemplateChanged(c)));
 13315            RenderEntries(entriesToRenderNow);
 13316        }
 317
 318        private void RenderEntries(IEnumerable<TemplEntry> entriesToRender)
 33319        {
 33320            var inputPaths = settingsProvider.GetSettings().ValidEntries
 66321                .Select(e => assetDatabase.GetAssetPath(e.InputAsset))
 322                .ToArray();
 33323            var templatePaths = settingsProvider.GetSettings().ValidEntries
 66324                .Select(e => assetDatabase.GetAssetPath(e.Template))
 325                .ToArray();
 326
 327            try
 33328            {
 33329                RenderEntriesWithProgress(entriesToRender, inputPaths, templatePaths);
 33330            }
 331            finally
 33332            {
 33333                editorUtility.ClearProgressBar();
 33334            }
 335
 33336            if (entriesToRender.Any() && settingsProvider.GetSettings().HasInvalidEntries)
 2337            {
 2338                log.Warn("Invalid live entries found in settings");
 2339            }
 33340        }
 341
 342        private void RenderEntriesWithProgress(
 343            IEnumerable<TemplEntry> entriesToRender,
 344            string[] inputPaths,
 345            string[] templatePaths)
 33346        {
 33347            var entryCount = entriesToRender.Count();
 348
 187349            foreach (var (entry, index) in entriesToRender.Select(GetEntryWithIndex))
 44350            {
 44351                editorUtility.DisplayProgressBar(
 352                    LiveEntriesTitle,
 353                    ProgressBarRenderingInfo,
 354                    (float)index / entryCount);
 44355                RenderEntry(entry, inputPaths, templatePaths);
 44356            }
 33357        }
 358
 359        private void RenderEntry(TemplEntry entry, string[] inputPaths, string[] templatePaths)
 44360        {
 44361            if (!CheckOverrides(entry, inputPaths, templatePaths))
 6362            {
 6363                return;
 364            }
 365
 38366            if (entry.ExposedInputName == NameOfOutputAssetPath)
 1367            {
 1368                log.Error("Entry input name must not be reserved " +
 369                    $"keyword: {NameOfOutputAssetPath}");
 1370                return;
 371            }
 372
 373            try
 37374            {
 37375                var context = GetContext(entry);
 37376                var template = Template.Parse(entry.Template.Text);
 37377                var result = template.Render(context);
 33378                fileSystem.WriteAllText(entry.FullPath, result);
 33379                assetDatabase.ImportAsset(entry.OutputAssetPath);
 33380                log.Info($"Template rendered at {entry.OutputAssetPath}");
 33381            }
 4382            catch (Exception e)
 4383            {
 4384                log.Error($"Error while rendering template at {entry.OutputAssetPath}", e);
 4385            }
 44386        }
 387
 388        private bool CheckOverrides(TemplEntry entry, string[] inputPaths, string[] templatePaths)
 44389        {
 44390            if (inputPaths.Contains(entry.OutputAssetPath))
 2391            {
 2392                log.Error("Overwriting an input in settings is not allowed. " +
 393                    $"Did not render template at {entry.OutputAssetPath}");
 2394                return false;
 395            }
 396
 42397            if (templatePaths.Contains(entry.OutputAssetPath))
 2398            {
 2399                log.Error("Overwriting a template in settings is not allowed. " +
 400                    $"Did not render template at {entry.OutputAssetPath}");
 2401                return false;
 402            }
 403
 40404            if (entry.OutputAssetPath == assetDatabase.GetAssetPath(settingsProvider.GetSettings()))
 2405            {
 2406                log.Error("Overwriting settings is not allowed. " +
 407                    $"Did not render template at {entry.OutputAssetPath}");
 2408                return false;
 409            }
 410
 38411            return true;
 44412        }
 413
 414        private TemplateContext GetContext(TemplEntry entry)
 37415        {
 37416            var scriptObject = new ScriptObject();
 1073417            scriptObject.Import(typeof(TemplFunctions), renamer: member => member.Name);
 37418            functions.ForEach(t => scriptObject.Import(t, renamer: member => member.Name));
 37419            scriptObject.Add(entry.ExposedInputName, entry.TheInputValue);
 37420            scriptObject.Add(NameOfOutputAssetPath, entry.OutputAssetPath);
 421
 37422            var context = new TemplateContext()
 423            {
 424                TemplateLoader = AssetTemplateLoader.Instance
 425            };
 426
 37427            context.PushGlobal(scriptObject);
 37428            return context;
 37429        }
 430
 431        private static bool IsMatchPath(string path, string entryPath) =>
 20432            entryPath == path ||
 433            GetAssetDirectoryPath(entryPath).StartsWith(path);
 434
 435        private static string GetAssetDirectoryPath(string path) =>
 8436            Path.GetDirectoryName(path).Replace(Path.DirectorySeparatorChar, AssetPathSeparator);
 437
 438        private static (TemplEntry, int) GetEntryWithIndex(TemplEntry entry, int index) =>
 44439            (entry, index);
 440
 441        private static AssetChange[] DecomposeChanges(AssetsPaths changes, TemplEntry entry)
 113442        {
 113443            var imported = entry.DeclaresChangeType(ChangeType.Import)
 113444                ? changes.importedAssets.Select(p => new AssetChange(ChangeType.Import, p))
 445                : EmptyAssetChangeArray;
 113446            var moved = entry.DeclaresChangeType(ChangeType.Move)
 447                ? changes.movedAssets.Select((p, i) =>
 113448                new AssetChange(ChangeType.Move, p, changes.movedFromAssetPaths[i]))
 449                : EmptyAssetChangeArray;
 113450            var deleted = entry.DeclaresChangeType(ChangeType.Delete)
 113451                ? changes.deletedAssets.Select(p => new AssetChange(ChangeType.Delete, p))
 452                : EmptyAssetChangeArray;
 113453            return imported
 454                .Union(moved)
 455                .Union(deleted)
 456                .ToArray();
 113457        }
 458    }
 459}

Methods/Properties

TemplEntryCore(Willykc.Templ.Editor.Abstraction.IAssetDatabase, Willykc.Templ.Editor.Abstraction.IFileSystem, Willykc.Templ.Editor.Abstraction.ISessionState, Willykc.Templ.Editor.Abstraction.ILogger, Willykc.Templ.Editor.Abstraction.ISettingsProvider, Willykc.Templ.Editor.Abstraction.ITemplateFunctionProvider, Willykc.Templ.Editor.Abstraction.IEditorUtility)
OnAssetsChanged(Willykc.Templ.Editor.AssetsPaths)
OnAfterAssemblyReload()
OnWillDeleteAsset(System.String)
FlagChangedEntry(Willykc.Templ.Editor.Entry.TemplEntry)
RenderAllValidEntries()
RenderEntry(System.String)
HandleRequestToRemoveLiveEntries(System.String, Willykc.Templ.Editor.TemplSettings, System.Collections.Generic.List[TemplEntry])
GetEagerDeferredEntries()
IsPathReferencedByEntry(Willykc.Templ.Editor.Entry.TemplEntry, System.String)
FunctionConflictsDetected()
IsSettingsChanged(Willykc.Templ.Editor.AssetsPaths)
RenderChangedEntries()
FlagDeferredEntries(System.Collections.Generic.IEnumerable[TemplEntry], Willykc.Templ.Editor.AssetsPaths)
FlagInputDeletedEntries(System.String)
RenderEagerEntries(System.Collections.Generic.IEnumerable[TemplEntry], Willykc.Templ.Editor.AssetsPaths)
RenderEntries(System.Collections.Generic.IEnumerable[TemplEntry])
RenderEntriesWithProgress(System.Collections.Generic.IEnumerable[TemplEntry], System.String[], System.String[])
RenderEntry(Willykc.Templ.Editor.Entry.TemplEntry, System.String[], System.String[])
CheckOverrides(Willykc.Templ.Editor.Entry.TemplEntry, System.String[], System.String[])
GetContext(Willykc.Templ.Editor.Entry.TemplEntry)
IsMatchPath(System.String, System.String)
GetAssetDirectoryPath(System.String)
GetEntryWithIndex(Willykc.Templ.Editor.Entry.TemplEntry, System.Int32)
DecomposeChanges(Willykc.Templ.Editor.AssetsPaths, Willykc.Templ.Editor.Entry.TemplEntry)