diff --git a/src/PAModelTests/DataSourceTests.cs b/src/PAModelTests/DataSourceTests.cs index 84a2aac7..5f7e7d59 100644 --- a/src/PAModelTests/DataSourceTests.cs +++ b/src/PAModelTests/DataSourceTests.cs @@ -9,7 +9,7 @@ using Microsoft.PowerPlatform.Formulas.Tools; using Microsoft.PowerPlatform.Formulas.Tools.Extensions; using Microsoft.PowerPlatform.Formulas.Tools.IO; -using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; +using Microsoft.PowerPlatform.PowerApps.Persistence; namespace PAModelTests; diff --git a/src/Persistence.Tests/MsApp/MsappArchiveTests.cs b/src/Persistence.Tests/MsApp/MsappArchiveTests.cs index 0751b182..89b18504 100644 --- a/src/Persistence.Tests/MsApp/MsappArchiveTests.cs +++ b/src/Persistence.Tests/MsApp/MsappArchiveTests.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System.IO.Compression; -using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; +using Microsoft.PowerPlatform.PowerApps.Persistence; +using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; +using Microsoft.PowerPlatform.PowerApps.Persistence.Utils; using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; namespace Persistence.Tests.MsApp; @@ -10,21 +12,21 @@ namespace Persistence.Tests.MsApp; [TestClass] public class MsappArchiveTests { - [DataRow(new string[] { "abc.txt" }, MsappArchive.ResourcesDirectory, 0)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.ResourcesDirectory}\abc.txt" }, MsappArchive.ResourcesDirectory, 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.ResourcesDirectory}\abc.txt" }, $@" \{MsappArchive.ResourcesDirectory}/", 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.ResourcesDirectory}/abc.txt" }, $@" {MsappArchive.ResourcesDirectory}/", 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.ResourcesDirectory}/abc.txt" }, $@" {MsappArchive.ResourcesDirectory}\", 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.ResourcesDirectory}\abc.txt" }, "NotFound", 0)] + [DataRow(new string[] { "abc.txt" }, MsappArchive.Directories.ResourcesDirectory, 0)] + [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.ResourcesDirectory}\abc.txt" }, MsappArchive.Directories.ResourcesDirectory, 1)] + [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.ResourcesDirectory}\abc.txt" }, $@" \{MsappArchive.Directories.ResourcesDirectory}/", 1)] + [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.ResourcesDirectory}/abc.txt" }, $@" {MsappArchive.Directories.ResourcesDirectory}/", 1)] + [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.ResourcesDirectory}/abc.txt" }, $@" {MsappArchive.Directories.ResourcesDirectory}\", 1)] + [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.ResourcesDirectory}\abc.txt" }, "NotFound", 0)] [DataRow(new string[] {"abc.txt", - @$"{MsappArchive.ResourcesDirectory}\abc.txt", - @$"ReSoUrCeS/efg.txt"}, MsappArchive.ResourcesDirectory, 2)] + @$"{MsappArchive.Directories.ResourcesDirectory}\abc.txt", + @$"ReSoUrCeS/efg.txt"}, MsappArchive.Directories.ResourcesDirectory, 2)] [DataRow(new string[] {"abc.txt", - @$"{MsappArchive.ResourcesDirectory}\abc.txt", - @$"{MsappArchive.ResourcesDirectory}/efg.txt"}, "RESOURCES", 2)] + @$"{MsappArchive.Directories.ResourcesDirectory}\abc.txt", + @$"{MsappArchive.Directories.ResourcesDirectory}/efg.txt"}, "RESOURCES", 2)] [DataRow(new string[] {"abc.txt", - @$"{MsappArchive.ResourcesDirectory}New\abc.txt", - @$"{MsappArchive.ResourcesDirectory}/efg.txt"}, MsappArchive.ResourcesDirectory, 1)] + @$"{MsappArchive.Directories.ResourcesDirectory}New\abc.txt", + @$"{MsappArchive.Directories.ResourcesDirectory}/efg.txt"}, MsappArchive.Directories.ResourcesDirectory, 1)] [TestMethod] public void GetDirectoryEntriesTests(string[] entries, string directoryName, int expectedDirectoryCount) { @@ -46,9 +48,9 @@ public void GetDirectoryEntriesTests(string[] entries, string directoryName, int } [DataRow(new string[] { "abc.txt" })] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.ResourcesDirectory}\abc.txt" })] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.ResourcesDirectory}\DEF.txt" })] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.ResourcesDirectory}\DEF.txt", @"\start-with-slash\test.json" })] + [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.ResourcesDirectory}\abc.txt" })] + [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.ResourcesDirectory}\DEF.txt" })] + [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.ResourcesDirectory}\DEF.txt", @"\start-with-slash\test.json" })] [TestMethod] public void AddEntryTests(string[] entries) { @@ -64,7 +66,7 @@ public void AddEntryTests(string[] entries) msappArchive.CanonicalEntries.Count.Should().Be(entries.Length); foreach (var entry in entries) { - msappArchive.CanonicalEntries.ContainsKey(MsappArchive.NormalizePath(entry)).Should().BeTrue(); + msappArchive.CanonicalEntries.ContainsKey(FileUtils.NormalizePath(entry)).Should().BeTrue(); } // Get the required entry should throw if it doesn't exist diff --git a/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs b/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs index 3b30b0f7..db73f98f 100644 --- a/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs +++ b/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; namespace Persistence.Tests.Yaml; @@ -10,12 +9,12 @@ namespace Persistence.Tests.Yaml; public class DeserializerInvalidTests { [TestMethod] - public void Deserialize_ShouldFail() + public void Deserialize_ShouldFailWhenYamlIsInvalid() { // Arrange var deserializer = YamlSerializationFactory.CreateDeserializer(); - var files = Directory.GetFiles(@"_TestData/InvalidYaml", $"*{MsappArchive.YamlFxFileExtension}", SearchOption.AllDirectories); + var files = Directory.GetFiles(@"_TestData/InvalidYaml", $"*{YamlUtils.YamlFxFileExtension}", SearchOption.AllDirectories); // Uncomment to test single file // var files = new string[] { @"_TestData/InvalidYaml/InvalidName.fx.yaml" }; diff --git a/src/Persistence.Tests/Yaml/ValidSerializerTests.cs b/src/Persistence.Tests/Yaml/ValidSerializerTests.cs index b61910b5..fd8fe3cb 100644 --- a/src/Persistence.Tests/Yaml/ValidSerializerTests.cs +++ b/src/Persistence.Tests/Yaml/ValidSerializerTests.cs @@ -98,4 +98,22 @@ public void Serialize_ShouldCreateValidYamlForCustomControl() var sut = serializer.Serialize(graph); sut.Should().Be($"Control: http://localhost/#customcontrol{Environment.NewLine}Name: CustomControl1{Environment.NewLine}Properties:{Environment.NewLine} Text: I am a custom control{Environment.NewLine}"); } + + [TestMethod] + public void Serialize_ShouldNotIncludeEnditorState() + { + var graph = new CustomControl("CustomControl1") + { + ControlUri = "http://localhost/#customcontrol", + EditorState = new() + { + Name = "CustomControl1", + }, + }; + + var serializer = YamlSerializationFactory.CreateSerializer(); + + var sut = serializer.Serialize(graph); + sut.Should().NotContain(nameof(Control.EditorState)); + } } diff --git a/src/Persistence/Extensions/IMsappArchiveExtensions.cs b/src/Persistence/Extensions/IMsappArchiveExtensions.cs new file mode 100644 index 00000000..3485f89a --- /dev/null +++ b/src/Persistence/Extensions/IMsappArchiveExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO.Compression; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; + +public static class IMsappArchiveExtensions +{ + /// + /// Returns the entry in the archive with the given name or throws a if it does not exist. + /// + /// the instance. + /// the name of the entry to fetch. + /// + /// + /// + public static ZipArchiveEntry GetRequiredEntry(this IMsappArchive archive, string entryName) + { + var entry = archive.GetEntry(entryName) ?? + throw new FileNotFoundException($"Entry '{entryName}' not found in msapp archive."); + + return entry; + } +} + diff --git a/src/Persistence/MsApp/IMsappArchive.cs b/src/Persistence/IMsappArchive.cs similarity index 70% rename from src/Persistence/MsApp/IMsappArchive.cs rename to src/Persistence/IMsappArchive.cs index c97b241e..9023bce6 100644 --- a/src/Persistence/MsApp/IMsappArchive.cs +++ b/src/Persistence/IMsappArchive.cs @@ -3,7 +3,7 @@ using System.IO.Compression; -namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; +namespace Microsoft.PowerPlatform.PowerApps.Persistence; /// /// base interface for MsappArchive @@ -26,6 +26,14 @@ public interface IMsappArchive /// the entry or null when not found. ZipArchiveEntry? GetEntry(string entryName); + /// + /// Returns all entries in the archive that are in the given directory. + /// + /// + /// + /// + IEnumerable GetDirectoryEntries(string directoryName, string? extension = null); + /// /// Provides access to the underlying zip archive. /// diff --git a/src/Persistence/MsApp/MsappArchive.cs b/src/Persistence/MsappArchive.cs similarity index 83% rename from src/Persistence/MsApp/MsappArchive.cs rename to src/Persistence/MsappArchive.cs index 113e3f29..41dbf1fb 100644 --- a/src/Persistence/MsApp/MsappArchive.cs +++ b/src/Persistence/MsappArchive.cs @@ -6,9 +6,12 @@ using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.PowerPlatform.PowerApps.Persistence.Models; +using Microsoft.PowerPlatform.PowerApps.Persistence.Utils; +using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; using YamlDotNet.Serialization; -namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; +namespace Microsoft.PowerPlatform.PowerApps.Persistence; + /// /// Represents a .msapp file. @@ -17,15 +20,6 @@ public class MsappArchive : IMsappArchive, IDisposable { #region Constants - public const string SrcDirectory = "Src"; - public const string ControlsDirectory = "Controls"; - public const string ComponentsDirectory = "Components"; - public const string AppTestDirectory = "AppTests"; - public const string ReferencesDirectory = "References"; - public const string ResourcesDirectory = "Resources"; - - public const string YamlFileExtension = ".yaml"; - public const string YamlFxFileExtension = ".fx.yaml"; public const string JsonFileExtension = ".json"; #endregion @@ -106,7 +100,7 @@ public MsappArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding foreach (var entry in ZipArchive.Entries) { - if (!canonicalEntries.TryAdd(NormalizePath(entry.FullName), entry)) + if (!canonicalEntries.TryAdd(FileUtils.NormalizePath(entry.FullName), entry)) _logger?.LogInformation($"Duplicate entry found in archive: {entry.FullName}"); } @@ -143,17 +137,12 @@ public MsappArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding #region Methods - /// - /// Returns all entries in the archive that are in the given directory. - /// - /// - /// - /// + /// public IEnumerable GetDirectoryEntries(string directoryName, string? extension = null) { _ = directoryName ?? throw new ArgumentNullException(nameof(directoryName)); - directoryName = NormalizePath(directoryName); + directoryName = FileUtils.NormalizePath(directoryName); foreach (var entry in CanonicalEntries) { @@ -173,35 +162,20 @@ public IEnumerable GetDirectoryEntries(string directoryName, st if (string.IsNullOrWhiteSpace(entryName)) return null; - entryName = NormalizePath(entryName); + entryName = FileUtils.NormalizePath(entryName); if (CanonicalEntries.TryGetValue(entryName, out var entry)) return entry; return null; } - /// - /// Returns the entry in the archive with the given name or throws if it does not exist. - /// - /// - /// - /// - /// - public ZipArchiveEntry GetRequiredEntry(string entryName) - { - var entry = GetEntry(entryName) ?? - throw new FileNotFoundException($"Entry '{entryName}' not found in msapp archive."); - - return entry; - } - /// public ZipArchiveEntry CreateEntry(string entryName) { if (string.IsNullOrWhiteSpace(entryName)) throw new ArgumentException("Entry name cannot be null or whitespace.", nameof(entryName)); - var canonicalEntryName = NormalizePath(entryName); + var canonicalEntryName = FileUtils.NormalizePath(entryName); if (_canonicalEntries.Value.ContainsKey(canonicalEntryName)) throw new InvalidOperationException($"Entry {entryName} already exists in the archive."); @@ -211,11 +185,6 @@ public ZipArchiveEntry CreateEntry(string entryName) return entry; } - public static string NormalizePath(string path) - { - return path.Trim().Replace('\\', '/').Trim('/').ToLowerInvariant(); - } - #endregion #region Private Methods @@ -225,7 +194,7 @@ private List LoadScreens() _logger?.LogInformation("Loading top level screens from Yaml."); var screens = new Dictionary(); - foreach (var yamlEntry in GetDirectoryEntries(Path.Combine(SrcDirectory, ControlsDirectory), YamlFileExtension)) + foreach (var yamlEntry in GetDirectoryEntries(Path.Combine(Directories.SrcDirectory, Directories.ControlsDirectory), YamlUtils.YamlFileExtension)) { using var textReader = new StreamReader(yamlEntry.Open()); try @@ -241,7 +210,7 @@ private List LoadScreens() _logger?.LogInformation("Loading top level controls editor state."); var controlEditorStates = new Dictionary(); - foreach (var editorStateEntry in GetDirectoryEntries(Path.Combine(ControlsDirectory), JsonFileExtension)) + foreach (var editorStateEntry in GetDirectoryEntries(Path.Combine(Directories.ControlsDirectory), JsonFileExtension)) { try { @@ -317,4 +286,15 @@ public void Dispose() } #endregion + + public static class Directories + { + public const string SrcDirectory = "Src"; + public const string ControlsDirectory = "Controls"; + public const string ComponentsDirectory = "Components"; + public const string AppTestDirectory = "AppTests"; + public const string ReferencesDirectory = "References"; + public const string ResourcesDirectory = "Resources"; + } } + diff --git a/src/Persistence/Utils/FileUtils.cs b/src/Persistence/Utils/FileUtils.cs new file mode 100644 index 00000000..4e318a6b --- /dev/null +++ b/src/Persistence/Utils/FileUtils.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.Utils; + +public static class FileUtils +{ + /// + /// Converts backslashes to forward slashes, removes trailing slashes, and converts to lowercase. + /// + /// + /// + public static string NormalizePath(string path) + { + return path.Trim().Replace('\\', '/').Trim('/').ToLowerInvariant(); + } +} diff --git a/src/Persistence/Yaml/YamlUtils.cs b/src/Persistence/Yaml/YamlUtils.cs new file mode 100644 index 00000000..27a86ba1 --- /dev/null +++ b/src/Persistence/Yaml/YamlUtils.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; + +public static class YamlUtils +{ + #region Constants + + public const string YamlFileExtension = ".yaml"; + public const string YmlFileExtension = ".yml"; + public const string YamlFxFileExtension = ".fx.yaml"; + + #endregion + + /// + /// Checks if the given path is a yaml file. + /// + /// + /// + public static bool IsYamlFile(string path) + { + return + Path.GetExtension(path).Equals(YamlFileExtension, StringComparison.OrdinalIgnoreCase) || + Path.GetExtension(path).Equals(YmlFileExtension, StringComparison.OrdinalIgnoreCase); + } +} +