< Summary

Class:Willykc.Templ.Editor.Scaffold.TemplScaffoldCore
Assembly:Willykc.Templ.Editor
File(s):/github/workspace/Packages/package.to.test/Editor/Scaffold/TemplScaffoldCore.cs
Covered lines:248
Uncovered lines:0
Coverable lines:248
Total lines:512
Line coverage:100% (248 of 248)
Covered branches:0
Total branches:0
Covered methods:20
Total methods:20
Method coverage:100% (20 of 20)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
TemplScaffoldCore(...)0%880100%
GenerateScaffold(...)0%440100%
ValidateScaffoldGeneration(...)0%110100%
ValidateScaffoldGeneration(...)0%440100%
CheckForEmptyScaffoldTree(...)0%440100%
CollectTemplateFunctionNameErrors(...)0%330100%
ProcessDynamicScaffold(...)0%550100%
GetShowProgressIncrementAction(...)0%110100%
GenerateScaffoldTree(...)0%880100%
CollectScaffoldErrors(...)0%880100%
CheckForDuplicateNodeNames(...)0%220100%
CollectRenderNameErrors(...)0%220100%
CheckForFileOverwrite(...)0%330100%
TryGetContext(...)0%110100%
CollectFileNodeErrors(...)0%330100%
IsNameDuplicated(...)0%330100%
ValidateRenderedName(...)0%330100%
GetTemplateContext(...)0%660100%
RenderTemplate(...)0%220100%
AddError(...)0%220100%

File(s)

/github/workspace/Packages/package.to.test/Editor/Scaffold/TemplScaffoldCore.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.Linq;
 27using UnityObject = UnityEngine.Object;
 28
 29namespace Willykc.Templ.Editor.Scaffold
 30{
 31    using Abstraction;
 32    using static TemplSettings;
 33
 34    internal sealed class TemplScaffoldCore : ITemplScaffoldCore
 35    {
 36        internal const string ScaffoldGenerationTitle = "Templ Scaffold Generation";
 37
 38        private const string ProgressBarValidatingInfo = "Validating...";
 39        private const string ProgressBarGeneratingInfo = "Generating...";
 40
 41        private readonly IFileSystem fileSystem;
 42        private readonly ILogger log;
 43        private readonly IEditorUtility editorUtility;
 44        private readonly List<Type> functions;
 45        private readonly string[] functionConflicts;
 46        private readonly string[] invalidFunctionNames;
 47
 3848        internal TemplScaffoldCore(
 49            IFileSystem fileSystem,
 50            ILogger log,
 51            IEditorUtility editorUtility,
 52            ITemplateFunctionProvider templateFunctionProvider)
 3853        {
 3854            this.fileSystem = fileSystem ??
 55                throw new ArgumentNullException(nameof(fileSystem));
 3856            this.log = log ??
 57                throw new ArgumentNullException(nameof(log));
 3858            this.editorUtility = editorUtility ??
 59                throw new ArgumentNullException(nameof(editorUtility));
 3860            templateFunctionProvider = templateFunctionProvider ??
 61                throw new ArgumentNullException(nameof(templateFunctionProvider));
 62
 3863            functions = templateFunctionProvider.GetTemplateFunctionTypes().ToList();
 3864            functionConflicts = templateFunctionProvider.GetDuplicateTemplateFunctionNames();
 3865            invalidFunctionNames = templateFunctionProvider.GetTemplateFunctionNames()
 266                .Where(fn => ReservedKeywords.Contains(fn))
 67                .ToArray();
 68
 3869            if (functionConflicts.Length > 0)
 670            {
 671                log.Error("Function name conflicts detected: " +
 72                    string.Join(", ", functionConflicts));
 673            }
 74
 3875            if(invalidFunctionNames.Length > 0)
 276            {
 277                log.Error($"Reserved keyword(s) used as function name: " +
 78                    string.Join(", ", invalidFunctionNames));
 279            }
 3880        }
 81
 82        string[] ITemplScaffoldCore.GenerateScaffold(
 83            TemplScaffold scaffold,
 84            string targetPath,
 85            object input,
 86            UnityObject selection,
 87            string[] skipPaths)
 1588        {
 1589            skipPaths ??= EmptyStringArray;
 1590            var errors = ValidateScaffoldGeneration(scaffold, targetPath, input, selection);
 91
 2092            if (errors.Count(e => e.Type != TemplScaffoldErrorType.Overwrite) > 0)
 593            {
 594                log.Error($"Found errors when generating {scaffold.name} scaffold at {targetPath}");
 595                return EmptyStringArray;
 96            }
 97
 1098            var paths = new List<string>();
 99
 10100            var showIncrement =
 101                GetShowProgressIncrementAction(scaffold.Root.NodeCount, ProgressBarGeneratingInfo);
 102
 103            try
 10104            {
 10105                GenerateScaffoldTree(scaffold.Root, targetPath, showIncrement, paths, skipPaths);
 10106            }
 107            finally
 10108            {
 10109                editorUtility.ClearProgressBar();
 10110            }
 111
 10112            log.Info($"{scaffold.name} scaffold generated successfully at {targetPath}");
 10113            return paths.ToArray();
 15114        }
 115
 116        TemplScaffoldError[] ITemplScaffoldCore.ValidateScaffoldGeneration(
 117            TemplScaffold scaffold,
 118            string targetPath,
 119            object input,
 120            UnityObject selection) =>
 19121            ValidateScaffoldGeneration(scaffold, targetPath, input, selection);
 122
 123        private TemplScaffoldError[] ValidateScaffoldGeneration(
 124            TemplScaffold scaffold,
 125            string targetPath,
 126            object input = null,
 127            UnityObject selection = null)
 34128        {
 34129            scaffold = scaffold
 130                ? scaffold
 131                : throw new ArgumentException($"{nameof(scaffold)} must not be null");
 34132            targetPath = !string.IsNullOrWhiteSpace(targetPath)
 133                ? targetPath
 134                : throw new ArgumentException($"{nameof(targetPath)} must not be null or empty");
 135
 34136            var validationContext = new ValidationContext()
 137            {
 138                rootPath = targetPath,
 139                seed = Guid.NewGuid().ToString(),
 140                input = input,
 141                path = targetPath,
 142                selection = selection,
 143                errors = new List<TemplScaffoldError>()
 144            };
 145
 34146            CollectTemplateFunctionNameErrors(validationContext);
 34147            ProcessDynamicScaffold(scaffold, validationContext);
 34148            CheckForEmptyScaffoldTree(scaffold, validationContext);
 149
 34150            if (validationContext.errors.Count > 0)
 9151            {
 9152                return validationContext.errors.ToArray();
 153            }
 154
 25155            var showProgressIncrement =
 156                GetShowProgressIncrementAction(scaffold.Root.NodeCount, ProgressBarValidatingInfo);
 157
 158            try
 25159            {
 25160                CollectScaffoldErrors(scaffold.Root, validationContext, showProgressIncrement);
 25161            }
 162            finally
 25163            {
 25164                editorUtility.ClearProgressBar();
 25165            }
 166
 25167            return validationContext.errors.ToArray();
 34168        }
 169
 170        private void CheckForEmptyScaffoldTree(
 171            TemplScaffold scaffold,
 172            ValidationContext validationContext)
 34173        {
 34174            if (scaffold.Root?.Children.Count == 0)
 1175            {
 1176                AddError(validationContext.errors, $"Found empty tree for scaffold {scaffold.name}",
 177                    TemplScaffoldErrorType.Undefined);
 1178            }
 34179        }
 180
 181        private void CollectTemplateFunctionNameErrors(ValidationContext validationContext)
 34182        {
 34183            var functionConflictErrors = functionConflicts
 3184                .Select(c => new TemplScaffoldError(TemplScaffoldErrorType.Undefined,
 185                $"Found duplicate template function name: {c}"));
 34186            var reservedKeywordErrors = invalidFunctionNames
 1187                .Select(rk => new TemplScaffoldError(TemplScaffoldErrorType.Undefined,
 188                $"Found reserved keyword used as function name: {rk}"));
 34189            validationContext.errors.AddRange(functionConflictErrors);
 34190            validationContext.errors.AddRange(reservedKeywordErrors);
 34191        }
 192
 193        private void ProcessDynamicScaffold(
 194            TemplScaffold scaffold,
 195            ValidationContext validationContext)
 34196        {
 34197            if (!(scaffold is TemplDynamicScaffold dynamicScaffold))
 23198            {
 23199                return;
 200            }
 201
 11202            if (!dynamicScaffold.TreeTemplate || dynamicScaffold.TreeTemplate.HasErrors)
 1203            {
 1204                AddError(validationContext.errors,
 205                    $"Null or invalid tree template for dynamic scaffold {scaffold.name}",
 206                    TemplScaffoldErrorType.Template);
 1207                return;
 208            }
 209
 10210            var templateContext = GetTemplateContext(validationContext);
 10211            var templateText = dynamicScaffold.TreeTemplate.Text;
 10212            var renderedText = string.Empty;
 213
 10214            if (string.IsNullOrWhiteSpace(templateText))
 1215            {
 1216                AddError(validationContext.errors,
 217                    $"Empty tree template for dynamic scaffold {scaffold.name}",
 218                    TemplScaffoldErrorType.Template);
 1219                return;
 220            }
 221
 222            try
 9223            {
 9224                var template = Template.Parse(templateText);
 9225                renderedText = template.Render(templateContext);
 8226                dynamicScaffold.Deserialize(renderedText);
 7227            }
 2228            catch (Exception e)
 2229            {
 2230                AddError(validationContext.errors,
 231                    "Error parsing tree for dynamic scaffold " +
 232                    $"{scaffold.name}:\n{renderedText}",
 233                    TemplScaffoldErrorType.Template, e);
 2234            }
 34235        }
 236
 237        private Action GetShowProgressIncrementAction(float total, string info)
 35238        {
 35239            float progress = 0;
 240
 241            void ShowProgressIncrement()
 128242            {
 128243                progress++;
 128244                editorUtility.DisplayProgressBar(ScaffoldGenerationTitle,
 245                    info, progress / total);
 128246            }
 247
 35248            return ShowProgressIncrement;
 35249        }
 250
 251        private void GenerateScaffoldTree(
 252            TemplScaffoldNode node,
 253            string targetNodePath,
 254            Action showProgressIncrement,
 255            List<string> paths,
 256            string[] skipPaths)
 30257        {
 30258            var renderedPath = node is TemplScaffoldRoot
 259                ? targetNodePath
 260                : $"{targetNodePath}/{node.RenderedName}";
 261
 262            try
 30263            {
 30264                if (node is TemplScaffoldDirectory && !fileSystem.DirectoryExists(renderedPath))
 8265                {
 8266                    fileSystem.CreateDirectory(renderedPath);
 8267                    paths.Add(renderedPath);
 8268                }
 22269                else if (node is TemplScaffoldFile fileNode && !skipPaths.Contains(renderedPath))
 10270                {
 10271                    fileSystem.WriteAllText(renderedPath, fileNode.RenderedTemplate);
 9272                    paths.Add(renderedPath);
 9273                }
 29274            }
 1275            catch (Exception e)
 1276            {
 1277                log.Error($"Error creating node {node.NodePath} at {renderedPath}", e);
 1278            }
 279
 30280            showProgressIncrement();
 281
 130282            foreach (var child in node.Children)
 20283            {
 20284                GenerateScaffoldTree(child, renderedPath, showProgressIncrement, paths, skipPaths);
 20285            }
 30286        }
 287
 288        private void CollectScaffoldErrors(
 289            TemplScaffoldNode node,
 290            ValidationContext validationContext,
 291            Action showProgressIncrement)
 98292        {
 98293            var templateContext = GetTemplateContext(validationContext);
 98294            CollectRenderNameErrors(node, validationContext, templateContext);
 295
 98296            validationContext.path = node is TemplScaffoldRoot
 297                ? validationContext.path
 298                : $"{validationContext.path}/{node.RenderedName}";
 299
 98300            CheckForFileOverwrite(node, validationContext);
 301
 98302            if (node is TemplScaffoldFile fileNode &&
 303                TryGetContext(fileNode, validationContext, out templateContext))
 33304            {
 33305                CollectFileNodeErrors(fileNode, templateContext, validationContext);
 33306            }
 65307            else if (node is TemplScaffoldDirectory && node.Children.Count == 0)
 3308            {
 3309                AddError(validationContext.errors, $"Empty directory node {node.NodePath}",
 310                    TemplScaffoldErrorType.Undefined);
 3311            }
 312
 98313            showProgressIncrement();
 98314            CheckForDuplicateNodeNames(node, validationContext);
 315
 440316            foreach (var child in node.Children)
 73317            {
 73318                CollectScaffoldErrors(child, validationContext, showProgressIncrement);
 73319            }
 98320        }
 321
 322        private void CheckForDuplicateNodeNames(
 323            TemplScaffoldNode node,
 324            ValidationContext validationContext)
 98325        {
 98326            if (IsNameDuplicated(node))
 4327            {
 4328                AddError(validationContext.errors,
 329                    "Different sister node with the same name found for node " +
 330                    node.NodePath, TemplScaffoldErrorType.Filename);
 4331            }
 98332        }
 333
 334        private void CollectRenderNameErrors(
 335            TemplScaffoldNode node,
 336            ValidationContext validationContext,
 337            TemplateContext context)
 98338        {
 98339            if (!(node is TemplScaffoldRoot))
 73340            {
 73341                node.RenderedName = RenderTemplate(node, node.name, context,
 342                TemplScaffoldErrorType.Filename, validationContext.errors);
 73343                ValidateRenderedName(node, validationContext.errors);
 73344            }
 98345        }
 346
 347        private void CheckForFileOverwrite(
 348            TemplScaffoldNode node,
 349            ValidationContext validationContext)
 98350        {
 98351            if (!(node is TemplScaffoldRoot) && fileSystem.FileExists(validationContext.path))
 1352            {
 1353                var error = new TemplScaffoldError(TemplScaffoldErrorType.Overwrite,
 354                    validationContext.path);
 1355                validationContext.errors.Add(error);
 1356            }
 98357        }
 358
 359        private bool TryGetContext(
 360            TemplScaffoldFile fileNode,
 361            ValidationContext validationContext,
 362            out TemplateContext templateContext)
 35363        {
 35364            templateContext = null;
 365
 366            try
 35367            {
 35368                templateContext = GetTemplateContext(validationContext, fileNode.NodeInputs);
 33369                return true;
 370            }
 2371            catch (Exception e)
 2372            {
 2373                AddError(validationContext.errors,
 374                    $"Error preparing context for node {fileNode.NodePath}",
 375                    TemplScaffoldErrorType.Undefined, e);
 2376                return false;
 377            }
 35378        }
 379
 380        private void CollectFileNodeErrors(
 381            TemplScaffoldFile fileNode,
 382            TemplateContext context,
 383            ValidationContext validationContext)
 33384        {
 33385            if (fileNode.Template && !fileNode.Template.HasErrors)
 32386            {
 32387                fileNode.RenderedTemplate = RenderTemplate(fileNode, fileNode.Template.Text,
 388                    context, TemplScaffoldErrorType.Template, validationContext.errors);
 32389            }
 390            else
 1391            {
 1392                AddError(validationContext.errors,
 393                    $"Null or invalid template found for node {fileNode.NodePath}",
 394                    TemplScaffoldErrorType.Template);
 1395            }
 33396        }
 397
 398        private static bool IsNameDuplicated(TemplScaffoldNode node) =>
 195399            node.Parent?.Children.Any(c => c != node && c.name == node.name) ?? false;
 400
 401        private void ValidateRenderedName(
 402            TemplScaffoldNode node,
 403            List<TemplScaffoldError> errors)
 73404        {
 73405            if (string.IsNullOrWhiteSpace(node.RenderedName))
 3406            {
 3407                AddError(errors, $"Empty {nameof(TemplScaffoldErrorType.Filename)} found for " +
 408                    $"node {node.NodePath}", TemplScaffoldErrorType.Filename);
 3409            }
 410
 73411            if (!node.RenderedName.IsValidFileName())
 4412            {
 4413                AddError(errors, "Invalid characters found in " +
 414                    $"{nameof(TemplScaffoldErrorType.Filename)}: {node.RenderedName} for " +
 415                    $"node {node.NodePath}", TemplScaffoldErrorType.Filename);
 4416            }
 73417        }
 418
 419        private TemplateContext GetTemplateContext(
 420            ValidationContext validationContext,
 421            IDictionary<string, object> nodeInputs = null)
 143422        {
 143423            nodeInputs ??= new Dictionary<string, object>();
 143424            var scriptObject = new ScriptObject();
 4147425            scriptObject.Import(typeof(TemplFunctions), renamer: member => member.Name);
 143426            functions.ForEach(t => scriptObject.Import(t, renamer: member => member.Name));
 143427            scriptObject.Add(InputName, validationContext.input);
 143428            scriptObject.Add(SelectionName, validationContext.selection);
 143429            scriptObject.Add(NameOfOutputAssetPath, validationContext.path);
 143430            scriptObject.Add(SeedName, validationContext.seed);
 143431            scriptObject.Add(RootPathName, validationContext.rootPath);
 432
 143433            var inputCollisions = nodeInputs.Keys.Where(scriptObject.ContainsKey).ToArray();
 143434            if (inputCollisions.Length > 0)
 2435            {
 2436                throw new InvalidOperationException("Found node input(s) named as reserved " +
 437                    "keywords: " + string.Join(", ", inputCollisions));
 438            }
 439
 447440            foreach (var nodeInput in nodeInputs)
 12441            {
 12442                scriptObject.Add(nodeInput.Key, nodeInput.Value);
 12443            }
 444
 141445            var templateContext = new TemplateContext()
 446            {
 447                TemplateLoader = AssetTemplateLoader.Instance
 448            };
 449
 141450            templateContext.PushGlobal(scriptObject);
 141451            return templateContext;
 141452        }
 453
 454        private string RenderTemplate(
 455            TemplScaffoldNode node,
 456            string text,
 457            TemplateContext context,
 458            TemplScaffoldErrorType errorType,
 459            List<TemplScaffoldError> errors)
 105460        {
 461            try
 105462            {
 105463                if (!string.IsNullOrWhiteSpace(text))
 104464                {
 104465                    var template = Template.Parse(text);
 104466                    return template.Render(context);
 467                }
 468                else
 1469                {
 1470                    AddError(errors, $"Empty {errorType} found in node {node.NodePath}", errorType);
 1471                }
 1472            }
 2473            catch (Exception e)
 2474            {
 2475                AddError(errors, $"Error rendering {errorType} of node {node.NodePath}",
 476                    errorType, e);
 2477            }
 478
 3479            return string.Empty;
 105480        }
 481
 482        private void AddError(
 483            List<TemplScaffoldError> errors,
 484            string message,
 485            TemplScaffoldErrorType type,
 486            Exception exception = null)
 25487        {
 25488            if (exception != null)
 6489            {
 6490                log.Error(message, exception);
 6491                message = $"{message}: {exception.Message}";
 6492            }
 493            else
 19494            {
 19495                log.Error(message);
 19496            }
 497
 25498            var error = new TemplScaffoldError(type, message);
 25499            errors.Add(error);
 25500        }
 501
 502        private struct ValidationContext
 503        {
 504            internal object input;
 505            internal UnityObject selection;
 506            internal string path;
 507            internal List<TemplScaffoldError> errors;
 508            internal string seed;
 509            internal string rootPath;
 510        }
 511    }
 512}

Methods/Properties

TemplScaffoldCore(Willykc.Templ.Editor.Abstraction.IFileSystem, Willykc.Templ.Editor.Abstraction.ILogger, Willykc.Templ.Editor.Abstraction.IEditorUtility, Willykc.Templ.Editor.Abstraction.ITemplateFunctionProvider)
GenerateScaffold(Willykc.Templ.Editor.Scaffold.TemplScaffold, System.String, System.Object, UnityEngine.Object, System.String[])
ValidateScaffoldGeneration(Willykc.Templ.Editor.Scaffold.TemplScaffold, System.String, System.Object, UnityEngine.Object)
ValidateScaffoldGeneration(Willykc.Templ.Editor.Scaffold.TemplScaffold, System.String, System.Object, UnityEngine.Object)
CheckForEmptyScaffoldTree(Willykc.Templ.Editor.Scaffold.TemplScaffold, Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext)
CollectTemplateFunctionNameErrors(Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext)
ProcessDynamicScaffold(Willykc.Templ.Editor.Scaffold.TemplScaffold, Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext)
GetShowProgressIncrementAction(System.Single, System.String)
GenerateScaffoldTree(Willykc.Templ.Editor.Scaffold.TemplScaffoldNode, System.String, System.Action, System.Collections.Generic.List[String], System.String[])
CollectScaffoldErrors(Willykc.Templ.Editor.Scaffold.TemplScaffoldNode, Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext, System.Action)
CheckForDuplicateNodeNames(Willykc.Templ.Editor.Scaffold.TemplScaffoldNode, Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext)
CollectRenderNameErrors(Willykc.Templ.Editor.Scaffold.TemplScaffoldNode, Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext, Scriban.TemplateContext)
CheckForFileOverwrite(Willykc.Templ.Editor.Scaffold.TemplScaffoldNode, Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext)
TryGetContext(Willykc.Templ.Editor.Scaffold.TemplScaffoldFile, Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext, Scriban.TemplateContext&)
CollectFileNodeErrors(Willykc.Templ.Editor.Scaffold.TemplScaffoldFile, Scriban.TemplateContext, Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext)
IsNameDuplicated(Willykc.Templ.Editor.Scaffold.TemplScaffoldNode)
ValidateRenderedName(Willykc.Templ.Editor.Scaffold.TemplScaffoldNode, System.Collections.Generic.List[TemplScaffoldError])
GetTemplateContext(Willykc.Templ.Editor.Scaffold.TemplScaffoldCore/ValidationContext, System.Collections.Generic.IDictionary[String,Object])
RenderTemplate(Willykc.Templ.Editor.Scaffold.TemplScaffoldNode, System.String, Scriban.TemplateContext, Willykc.Templ.Editor.Scaffold.TemplScaffoldErrorType, System.Collections.Generic.List[TemplScaffoldError])
AddError(System.Collections.Generic.List[TemplScaffoldError], System.String, Willykc.Templ.Editor.Scaffold.TemplScaffoldErrorType, System.Exception)