diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 94d6b357..b827a760 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,11 +9,12 @@ on: jobs: build: - runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.os == 'ubuntu-latest' }} strategy: matrix: - os: [windows-latest, macos-latest, ubuntu-latest] + os: [windows-latest, ubuntu-latest] + + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.os != 'Windows' }} env: DOTNET_NOLOGO: true @@ -25,7 +26,6 @@ jobs: - uses: actions/setup-dotnet@v4 with: dotnet-version: | - 7.0.x 8.0.x - run: dotnet --info @@ -36,7 +36,7 @@ jobs: if: ${{ runner.os == 'Windows' }} ## Only difference here is not forcing cmd - - name: Build (ubuntu & macos) + - name: Build (linux based) run: ./build ci if: ${{ runner.os != 'Windows' }} @@ -45,13 +45,3 @@ jobs: - name: Test - PAModel run: dotnet bin/Debug/PAModelTests/PAModelTests.dll - - - name: Restore workloads for samples - run: dotnet workload restore samples/MauiMsApp/MauiMsApp.csproj - - - name: Install maui-tizen workload - run: dotnet workload install maui-tizen - - - name: Build samples - run: dotnet build samples/samples.sln - if: ${{ runner.os == 'Windows' }} diff --git a/samples/README.md b/samples/README.md index eec289b8..045762b1 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,5 +1,7 @@ # Samples for Power Apps +> NOTICE: This samples folder depends on the no longer supported 'Preview Schema' (aka PaYamlV2) and is no longer supported or built. + Samples for creating, reading, and updating files containing Power Apps (*.msapp) ## Prerequisites @@ -31,4 +33,4 @@ List of screens in the MsApp file: ## Potential Errors If you see the following Error: "The project doesn't know how to run the profile with the name 'Windows Machine' and command 'MsixPackage'." -ensure that you have '.NET Multi-platform App UI development' installed through Visual Studio. \ No newline at end of file +ensure that you have '.NET Multi-platform App UI development' installed through Visual Studio. diff --git a/src/Persistence.Tests/Extensions/IYamlDeserializerExtensions.cs b/src/Persistence.Tests/Extensions/IYamlDeserializerExtensions.cs deleted file mode 100644 index ada764e9..00000000 --- a/src/Persistence.Tests/Extensions/IYamlDeserializerExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -namespace Persistence.Tests.Extensions; - -internal static class YamlDeserializerExtensions -{ - internal static object? DeserializeControl(this IYamlDeserializer deserializer, TextReader yamlReader, Type controlType) - { - var deserializeMethod = typeof(IYamlDeserializer).GetMethod(nameof(IYamlDeserializer.Deserialize), types: new[] { typeof(TextReader) })!.MakeGenericMethod(controlType); - return deserializeMethod.Invoke(deserializer, new object[] { yamlReader }); - } -} diff --git a/src/Persistence.Tests/Model/CustomPropertyTests.cs b/src/Persistence.Tests/Model/CustomPropertyTests.cs deleted file mode 100644 index 92f17348..00000000 --- a/src/Persistence.Tests/Model/CustomPropertyTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Persistence.Tests.Model; - -[TestClass] -public class CustomPropertyTests -{ - [TestMethod] - [DataRow(CustomProperty.PropertyType.Data, PropertyCategory.Data)] - [DataRow(CustomProperty.PropertyType.Event, PropertyCategory.Behavior)] - [DataRow(CustomProperty.PropertyType.Function, PropertyCategory.Data)] - [DataRow(CustomProperty.PropertyType.Action, PropertyCategory.Behavior)] - public void Category_shouldBeValidBasedOnType(CustomProperty.PropertyType propertyType, PropertyCategory expectedCategory) - { - var sut = new CustomProperty - { - Name = "Test", - Type = propertyType - }; - - // Assert - sut.Category.Should().Be(expectedCategory); - } -} diff --git a/src/Persistence.Tests/Model/InvalidControls.cs b/src/Persistence.Tests/Model/InvalidControls.cs deleted file mode 100644 index 33b2072e..00000000 --- a/src/Persistence.Tests/Model/InvalidControls.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Persistence.Tests.Model; - -[TestClass] -public class InvalidControls : TestBase -{ - [TestMethod] - [DataRow("")] - [DataRow(" ")] - public void Constructor_InvalidControlName_Throws(string controlName) - { - // Act - Action act = () => ControlFactory.CreateScreen(controlName); - - // Assert - act.Should().Throw(); - } -} diff --git a/src/Persistence.Tests/MsApp/MsappArchiveSaveTests.cs b/src/Persistence.Tests/MsApp/MsappArchiveSaveTests.cs deleted file mode 100644 index dc46bdaf..00000000 --- a/src/Persistence.Tests/MsApp/MsappArchiveSaveTests.cs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; - -namespace Persistence.Tests.MsApp; - -[TestClass] -public class MsappArchiveSaveTests : TestBase -{ - [TestMethod] - [DataRow(@" Hello ", $"src/Hello.pa.yaml", @"_TestData/ValidYaml-CI/Screen-Hello1.pa.yaml")] - [DataRow(@"..\..\Hello", $"src/....Hello.pa.yaml", @"_TestData/ValidYaml-CI/Screen-Hello2.pa.yaml")] - [DataRow(@"c:\win\..\..\Hello", $"src/cWin....Hello.pa.yaml", @"_TestData/ValidYaml-CI/Screen-Hello3.pa.yaml")] - [DataRow(@"//..?HelloScreen", $"src/..HelloScreen.pa.yaml", @"_TestData/ValidYaml-CI/Screen-Hello4.pa.yaml")] - [DataRow(@"Hello Space", $"src/Hello Space.pa.yaml", @"_TestData/ValidYaml-CI/Screen-Hello5.pa.yaml")] - public void Msapp_ShouldSave_Screen(string screenName, string screenEntryName, string expectedYamlPath) - { - // Arrange - var tempFile = Path.Combine(TestContext.DeploymentDirectory!, Path.GetRandomFileName()); - using var msappArchive = MsappArchiveFactory.Create(tempFile); - - msappArchive.App.Should().BeNull(); - - // Act - var screen = ControlFactory.CreateScreen(screenName); - msappArchive.Save(screen); - msappArchive.Dispose(); - - // Assert - using var msappValidation = MsappArchiveFactory.Open(tempFile); - msappValidation.App.Should().BeNull(); - msappValidation.CanonicalEntries.Count.Should().Be(2); - msappValidation.DoesEntryExist(screenEntryName).Should().BeTrue(); - using var streamReader = new StreamReader(msappValidation.GetRequiredEntry(screenEntryName).Open()); - var yaml = streamReader.ReadToEnd().NormalizeNewlines(); - var expectedYaml = File.ReadAllText(expectedYamlPath).NormalizeNewlines(); - yaml.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@" Hello ", "My control", - $"src/Hello.pa.yaml", - $"{MsappArchive.Directories.Controls}/Hello.json", - @"_TestData/ValidYaml-CI/Screen-with-control1.pa.yaml", - @"_TestData/ValidYaml-CI/Screen-with-control1.json")] - public void Msapp_ShouldSave_Screen_With_Control(string screenName, string controlName, string screenEntryName, string editorStateName, - string expectedYamlPath, string expectedJsonPath) - { - // Arrange - var tempFile = Path.Combine(TestContext.DeploymentDirectory!, Path.GetRandomFileName()); - using var msappArchive = MsappArchiveFactory.Create(tempFile); - - msappArchive.App.Should().BeNull(); - - // Act - var screen = ControlFactory.CreateScreen(screenName, - children: new[] { - ControlFactory.Create(controlName, "ButtonCanvas") - }); - msappArchive.Save(screen); - msappArchive.Dispose(); - - // Assert - using var msappValidation = MsappArchiveFactory.Open(tempFile); - msappValidation.App.Should().BeNull(); - msappValidation.CanonicalEntries.Count.Should().Be(2); - - // Validate screen - msappValidation.DoesEntryExist(screenEntryName).Should().BeTrue(); - using var streamReader = new StreamReader(msappValidation.GetRequiredEntry(screenEntryName).Open()); - var yaml = streamReader.ReadToEnd().NormalizeNewlines(); - var expectedYaml = File.ReadAllText(expectedYamlPath).NormalizeNewlines(); - yaml.Should().Be(expectedYaml); - - // Validate editor state - if (msappValidation.DoesEntryExist(editorStateName)) - { - using var editorStateReader = new StreamReader(msappValidation.GetRequiredEntry(editorStateName).Open()); - var json = editorStateReader.ReadToEnd().ReplaceLineEndings(); - var expectedJson = File.ReadAllText(expectedJsonPath).ReplaceLineEndings().TrimEnd(); - json.Should().Be(expectedJson); - } - } - - - [TestMethod] - [DataRow("HelloScreen")] - public void Msapp_ShouldSave_App(string screenName) - { - // Arrange - var tempFile = Path.Combine(TestContext.DeploymentDirectory!, Path.GetRandomFileName()); - using (var msappArchive = MsappArchiveFactory.Create(tempFile)) - { - msappArchive.App.Should().BeNull(); - - // Act - var app = ControlFactory.CreateApp(); - app.Screens.Add(ControlFactory.CreateScreen(screenName)); - msappArchive.App = app; - - msappArchive.Save(); - } - - // Assert - using var msappValidation = MsappArchiveFactory.Open(tempFile); - msappValidation.App.Should().NotBeNull(); - msappValidation.App!.Screens.Count.Should().Be(1); - msappValidation.App.Screens.Single().Name.Should().Be(screenName); - msappValidation.App.Name.Should().Be(App.ControlName); - msappValidation.DoesEntryExist(MsappArchive.HeaderFileName).Should().BeTrue(); - } - - [TestMethod] - public void Msapp_ShouldSave_WithUniqueName() - { - // Arrange - var tempFile = Path.Combine(TestContext.DeploymentDirectory!, Path.GetRandomFileName()); - using var msappArchive = MsappArchiveFactory.Create(tempFile); - - var sameNames = new string[] { "SameName", "Same..Name", @"..\SameName", "!SameName!", ".SameName", "SameName", "SAMENAME", "SameNAME", "SameName1", "{SameName}" }; - - // Act - for (var idx = 0; idx < sameNames.Length; idx++) - { - var screen = ControlFactory.CreateScreen(sameNames[idx]); - msappArchive.Save(screen); - - // Assert - msappArchive.CanonicalEntries.Count.Should().Be(idx + 1); - } - - msappArchive.CanonicalEntries.Count.Should().Be(sameNames.Length); - msappArchive.DoesEntryExist(Path.Combine(MsappArchive.Directories.Src, @$"SameName{sameNames.Length + 1}.pa.yaml")).Should().BeTrue(); - } -} diff --git a/src/Persistence.Tests/MsApp/MsappArchiveTests.cs b/src/Persistence.Tests/MsApp/MsappArchiveTests.cs index ee6d47db..1369a561 100644 --- a/src/Persistence.Tests/MsApp/MsappArchiveTests.cs +++ b/src/Persistence.Tests/MsApp/MsappArchiveTests.cs @@ -4,45 +4,34 @@ using System.IO.Compression; using Microsoft.PowerPlatform.PowerApps.Persistence; using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; -using Moq; namespace Persistence.Tests.MsApp; [TestClass] public class MsappArchiveTests : TestBase { - private readonly Mock _mockYamlSerializationFactory; - - public MsappArchiveTests() - { - _mockYamlSerializationFactory = new(MockBehavior.Strict); - _mockYamlSerializationFactory.Setup(f => f.CreateSerializer(It.IsAny())) - .Returns(new Mock(MockBehavior.Strict).Object); - _mockYamlSerializationFactory.Setup(f => f.CreateDeserializer(It.IsAny())) - .Returns(new Mock(MockBehavior.Strict).Object); - } - - [DataRow(new string[] { "abc.txt" }, MsappArchive.Directories.Resources, null, 0, 0)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.Resources}\", @$"{MsappArchive.Directories.Resources}\abc.txt" }, null, null, 1, 2)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.Resources}\", @$"{MsappArchive.Directories.Resources}\abc.txt" }, null, ".txt", 1, 2)] - [DataRow(new string[] { "abc.txt", "def.txt", @$"{MsappArchive.Directories.Resources}\", @$"{MsappArchive.Directories.Resources}\abc.txt" }, null, ".txt", 2, 3)] - [DataRow(new string[] { "abc.jpg", @$"{MsappArchive.Directories.Resources}\", @$"{MsappArchive.Directories.Resources}\abc.txt" }, null, ".txt", 0, 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.Resources}\abc.txt" }, MsappArchive.Directories.Resources, null, 1, 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.Resources}\abc.txt" }, $@" {MsappArchive.Directories.Resources}/ ", ".txt", 1, 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.Resources}/abc.txt", @$"{MsappArchive.Directories.Resources}/qwe.jpg" }, - $@" {MsappArchive.Directories.Resources}/", ".jpg", 1, 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.Resources}/abc.txt" }, $@" {MsappArchive.Directories.Resources}\", null, 1, 1)] - [DataRow(new string[] { "abc.txt", @$"{MsappArchive.Directories.Resources}\abc.txt" }, "NotFound", "*.txt", 0, 0)] + private const string ResourcesDirectoryName = "Resources"; + + [DataRow(new string[] { "abc.txt" }, ResourcesDirectoryName, null, 0, 0)] + [DataRow(new string[] { "abc.txt", @$"{ResourcesDirectoryName}\", @$"{ResourcesDirectoryName}\abc.txt" }, null, null, 1, 2)] + [DataRow(new string[] { "abc.txt", @$"{ResourcesDirectoryName}\", @$"{ResourcesDirectoryName}\abc.txt" }, null, ".txt", 1, 2)] + [DataRow(new string[] { "abc.txt", "def.txt", @$"{ResourcesDirectoryName}\", @$"{ResourcesDirectoryName}\abc.txt" }, null, ".txt", 2, 3)] + [DataRow(new string[] { "abc.jpg", @$"{ResourcesDirectoryName}\", @$"{ResourcesDirectoryName}\abc.txt" }, null, ".txt", 0, 1)] + [DataRow(new string[] { "abc.txt", @$"{ResourcesDirectoryName}\abc.txt" }, ResourcesDirectoryName, null, 1, 1)] + [DataRow(new string[] { "abc.txt", @$"{ResourcesDirectoryName}\abc.txt" }, $@" {ResourcesDirectoryName}/ ", ".txt", 1, 1)] + [DataRow(new string[] { "abc.txt", @$"{ResourcesDirectoryName}/abc.txt", @$"{ResourcesDirectoryName}/qwe.jpg" }, + $@" {ResourcesDirectoryName}/", ".jpg", 1, 1)] + [DataRow(new string[] { "abc.txt", @$"{ResourcesDirectoryName}/abc.txt" }, $@" {ResourcesDirectoryName}\", null, 1, 1)] + [DataRow(new string[] { "abc.txt", @$"{ResourcesDirectoryName}\abc.txt" }, "NotFound", "*.txt", 0, 0)] [DataRow(new string[] {"abc.txt", - @$"{MsappArchive.Directories.Resources}\abc.txt", - @$"ReSoUrCeS/efg.txt"}, MsappArchive.Directories.Resources, null, 2, 2)] + @$"{ResourcesDirectoryName}\abc.txt", + @$"ReSoUrCeS/efg.txt"}, ResourcesDirectoryName, null, 2, 2)] [DataRow(new string[] {"abc.txt", - @$"{MsappArchive.Directories.Resources}\abc.txt", - @$"{MsappArchive.Directories.Resources}/efg.txt"}, "RESOURCES", null, 2, 2)] + @$"{ResourcesDirectoryName}\abc.txt", + @$"{ResourcesDirectoryName}/efg.txt"}, "RESOURCES", null, 2, 2)] [DataRow(new string[] {"abc.txt", - @$"{MsappArchive.Directories.Resources}New\abc.txt", - @$"{MsappArchive.Directories.Resources}/efg.txt"}, MsappArchive.Directories.Resources, null, 1, 1)] + @$"{ResourcesDirectoryName}New\abc.txt", + @$"{ResourcesDirectoryName}/efg.txt"}, ResourcesDirectoryName, null, 1, 1)] [TestMethod] public void GetDirectoryEntriesTests(string[] entries, string directoryName, string extension, int expectedCount, int expectedRecursiveCount) { @@ -77,7 +66,7 @@ public void AddEntryTests(string[] entries, string[] expectedEntries) } // Assert - msappArchive.CanonicalEntries.Count.Should().Be(entries.Length); + msappArchive.CanonicalEntries().Count.Should().Be(entries.Length); foreach (var expectedEntry in expectedEntries) { msappArchive.DoesEntryExist(expectedEntry).Should().BeTrue($"Expected entry {expectedEntry} to exist in the archive"); @@ -96,48 +85,19 @@ private static IEnumerable AddEntryTestsData() [ "abc.txt" ] }; yield return new string[][] { - [ "abc.txt", @$"{MsappArchive.Directories.Resources}\abc.txt" ], - [ "abc.txt", @$"{MsappArchive.Directories.Resources}/abc.txt".ToLowerInvariant() ], + [ "abc.txt", @$"{ResourcesDirectoryName}\abc.txt" ], + [ "abc.txt", @$"{ResourcesDirectoryName}/abc.txt".ToLowerInvariant() ], }; yield return new string[][] { - [ "abc.txt", @$"{MsappArchive.Directories.Resources}\DEF.txt" ], - [ "abc.txt", @$"{MsappArchive.Directories.Resources}/DEF.txt".ToLowerInvariant() ], + [ "abc.txt", @$"{ResourcesDirectoryName}\DEF.txt" ], + [ "abc.txt", @$"{ResourcesDirectoryName}/DEF.txt".ToLowerInvariant() ], }; yield return new string[][] { - [ "abc.txt", @$"{MsappArchive.Directories.Resources}\DEF.txt", @"\start-with-slash\test.json" ], - [ "abc.txt", @$"{MsappArchive.Directories.Resources}/DEF.txt".ToLowerInvariant(), @"start-with-slash/test.json" ], + [ "abc.txt", @$"{ResourcesDirectoryName}\DEF.txt", @"\start-with-slash\test.json" ], + [ "abc.txt", @$"{ResourcesDirectoryName}/DEF.txt".ToLowerInvariant(), @"start-with-slash/test.json" ], }; } - [TestMethod] - [DataRow(@"_TestData/AppsWithYaml/HelloWorld.msapp", 12, 1, "HelloScreen", "screen", 8)] - public void Msapp_ShouldHave_Screens(string testDirectory, int allEntriesCount, int controlsCount, - string topLevelControlName, string topLevelControlType, - int topLevelRulesCount) - { - // Zip archive in memory from folder - using var stream = new MemoryStream(); - using (var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, true)) - { - var files = Directory.GetFiles(testDirectory, "*", SearchOption.AllDirectories); - foreach (var file in files) - { - zipArchive.CreateEntryFromFile(file, file.Substring(testDirectory.Length + 1)); - } - } - - // Arrange & Act - using var msappArchive = MsappArchiveFactory.Open(stream); - - // Assert - msappArchive.CanonicalEntries.Count.Should().Be(allEntriesCount); - msappArchive.App.Should().NotBeNull(); - msappArchive.App!.Screens.Count.Should().Be(controlsCount); - msappArchive.Version.Should().Be(Version.Parse("2.2")); - - var screen = msappArchive.App.Screens.Single(c => c.Name == topLevelControlName); - } - [TestMethod] public void ZipArchiveEntryPathTests() { @@ -168,7 +128,7 @@ public void DoesEntryExistTests() { // Setup test archive with a couple entries in it already using var archiveMemStream = new MemoryStream(); - using var archive = new MsappArchive(archiveMemStream, ZipArchiveMode.Create, _mockYamlSerializationFactory.Object); + using var archive = new MsappArchive(archiveMemStream, ZipArchiveMode.Create); archive.CreateEntry("entryA"); archive.CreateEntry("entryB"); archive.CreateEntry("dir1/entryA"); @@ -202,7 +162,7 @@ public void DoesEntryExistWorksWithNewEntriesCreated() { // Setup test archive with a couple entries in it already using var archiveMemStream = new MemoryStream(); - using var archive = new MsappArchive(archiveMemStream, ZipArchiveMode.Create, _mockYamlSerializationFactory.Object); + using var archive = new MsappArchive(archiveMemStream, ZipArchiveMode.Create); archive.CreateEntry("entryA"); archive.CreateEntry("entryB"); archive.CreateEntry("dir2/entryA"); @@ -219,7 +179,7 @@ public void GenerateUniqueEntryPathTests() { // Setup test archive with a couple entries in it already using var archiveMemStream = new MemoryStream(); - using var archive = new MsappArchive(archiveMemStream, ZipArchiveMode.Create, _mockYamlSerializationFactory.Object); + using var archive = new MsappArchive(archiveMemStream, ZipArchiveMode.Create); archive.CreateEntry("entryA.pa.yaml"); archive.CreateEntry("entryB.pa.yaml"); archive.CreateEntry("dir1/entryC.pa.yaml"); @@ -250,7 +210,7 @@ public void GenerateUniqueEntryPathReturnsNormalizedPathsTests() { // Setup test archive with a couple entries in it already using var archiveMemStream = new MemoryStream(); - using var archive = new MsappArchive(archiveMemStream, ZipArchiveMode.Create, _mockYamlSerializationFactory.Object); + using var archive = new MsappArchive(archiveMemStream, ZipArchiveMode.Create); archive.CreateEntry("entryA.pa.yaml"); archive.CreateEntry("dir1/entryA.pa.yaml"); archive.CreateEntry("dir1/dir2/entryA.pa.yaml"); diff --git a/src/Persistence.Tests/MsApp/RoundTripWriterTests.cs b/src/Persistence.Tests/MsApp/RoundTripWriterTests.cs deleted file mode 100644 index 78320783..00000000 --- a/src/Persistence.Tests/MsApp/RoundTripWriterTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence; -using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; - -namespace Persistence.Tests.MsApp; - -[TestClass] -public class RoundTripWriterTests -{ - [TestMethod] - [DataRow("input", "input")] - [DataRow("input : string", "input : string")] - [DataRow("", "")] - [DataRow("\r\n", "\n")] - [DataRow("\n", "\r\n")] - [DataRow("input : \nstring", "input : \nstring")] - [DataRow("input : \r\nstring", "input : \nstring")] - [DataRow("input : \r\nstring", "input : \r\nstring")] - [DataRow("input : \nstring", "input : \r\nstring")] - public void Write_Should_Be_Identical(string input, string output) - { - // Arrange - var inputReader = new StringReader(input); - using var roundTripWriter = new RoundTripWriter(inputReader, "test"); - - // Act - foreach (var c in output) - { - roundTripWriter.Write(c); - } - } - - [TestMethod] - [DataRow(" ab", "ab", 1, 1)] - [DataRow("ab", " ab", 1, 1)] - [DataRow("input", "input2", 1, 6)] - [DataRow("input2", "input", 1, 5)] - [DataRow("input : value", "input : value ", 1, 14)] - [DataRow("input : value ", "input : value", 1, 13)] - [DataRow("input : \r\nvalue ", "input : \r\nvalue", 2, 5)] - [DataRow("input : \r\nvalue", "input : \r\nValue", 2, 1)] - [DataRow("input : \r\nvalue", "input : \nValue", 2, 1)] - public void Write_Should_Fail(string input, string output, int line, int column) - { - // Arrange - var inputReader = new StringReader(input); - - // Act - Action act = () => - { - using var roundTripWriter = new RoundTripWriter(inputReader, "test"); - foreach (var c in output) - { - roundTripWriter.Write(c); - }; - }; - - // Assert - var thrownEx = act.Should().ThrowExactly() - .WithErrorCode(PersistenceErrorCode.RoundTripValidationFailed) - .Which; - thrownEx.MsappEntryFullPath.Should().Be("test"); - thrownEx.LineNumber.Should().Be(line); - thrownEx.Column.Should().Be(column); - } -} diff --git a/src/Persistence.Tests/Persistence.Tests.csproj b/src/Persistence.Tests/Persistence.Tests.csproj index 215bc194..493b649c 100644 --- a/src/Persistence.Tests/Persistence.Tests.csproj +++ b/src/Persistence.Tests/Persistence.Tests.csproj @@ -1,4 +1,4 @@ - + $(TargetFrameworkVersion) @@ -33,13 +33,13 @@ + - diff --git a/src/Persistence.Tests/Templates/ControlFactoryTests.cs b/src/Persistence.Tests/Templates/ControlFactoryTests.cs deleted file mode 100644 index fcb4782e..00000000 --- a/src/Persistence.Tests/Templates/ControlFactoryTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -namespace Persistence.Tests.Templates; - -[TestClass] -public class ControlFactoryTests : TestBase -{ - [TestMethod] - public void CreateFromTemplate_ShouldSetControlPropertiesWhenAvailable() - { - var expectedProps = new ControlPropertiesCollection - { - { "Property1", "Value1" }, - { "Property2", "Value2" } - }; - - var template = ControlTemplateStore.GetByName("Screen"); - - var sut = new ControlFactory(ControlTemplateStore); - - var result = sut.Create("Screen", template, properties: expectedProps); - result.Should().NotBeNull(); - result.Properties.Should().NotBeNull() - .And.BeEquivalentTo(expectedProps); - } - - [TestMethod] - public void CreateFromTemplate_ShouldNotSetControlPropertiesWhenNotAvailable() - { - var template = ControlTemplateStore.GetByName("Screen"); - - var sut = new ControlFactory(ControlTemplateStore); - - var result = sut.Create("Screen", template, properties: null); - result.Should().NotBeNull(); - result.Properties.Should().NotBeNull() - .And.BeEmpty(); - } - - [TestMethod] - public void CreateFromTemplateName_Should_CreateFirstClassTypes() - { - var sut = new ControlFactory(ControlTemplateStore); - - var result = sut.Create("Screen1", "Screen", properties: null); - result.Should().NotBeNull().And.BeOfType(); - result.Properties.Should().NotBeNull().And.BeEmpty(); - result.Children.Should().BeNull(); - } - - [TestMethod] - [DataRow("Component", "MyComponent1", "http://microsoft.com/appmagic/Component", ComponentType.Canvas)] - [DataRow("CommandComponent", "MyComponent1", "http://microsoft.com/appmagic/CommandComponent", ComponentType.Command)] - [DataRow("DataComponent", "MyComponent1", "http://microsoft.com/appmagic/DataComponent", ComponentType.Data)] - [DataRow("FunctionComponent", "MyComponent1", "http://microsoft.com/appmagic/FunctionComponent", ComponentType.Function)] - public void CreateComponent_ShouldCreateValidInstance(string componentType, string componentName, string expectedTemplateId, ComponentType expectedType) - { - var sut = new ControlFactory(ControlTemplateStore); - - var result = sut.Create(componentName, componentType); - result.Should().NotBeNull().And.BeAssignableTo(); - - var componendDef = (ComponentDefinition)result; - componendDef.Name.Should().Be(componentName); - componendDef.Template.Should().NotBeNull(); - componendDef.Template.Id.Should().Be(expectedTemplateId); - componendDef.Template.Name.Should().Be(componentName); - componendDef.Properties.Should().NotBeNull().And.BeEmpty(); - componendDef.Children.Should().BeNull(); - componendDef.Type.Should().Be(expectedType); - } -} diff --git a/src/Persistence.Tests/TestBase.cs b/src/Persistence.Tests/TestBase.cs index ecad3a63..8bd6aaf5 100644 --- a/src/Persistence.Tests/TestBase.cs +++ b/src/Persistence.Tests/TestBase.cs @@ -2,11 +2,8 @@ // Licensed under the MIT License. using System.Globalization; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; namespace Persistence.Tests; @@ -14,12 +11,8 @@ public abstract class TestBase : VSTestBase { public static IServiceProvider ServiceProvider { get; set; } - public IControlTemplateStore ControlTemplateStore { get; private set; } - public IMsappArchiveFactory MsappArchiveFactory { get; private set; } - public IControlFactory ControlFactory { get; private set; } - static TestBase() { ServiceProvider = BuildServiceProvider(); @@ -28,132 +21,23 @@ static TestBase() public TestBase() { // Request commonly used services - ControlTemplateStore = ServiceProvider.GetRequiredService(); MsappArchiveFactory = ServiceProvider.GetRequiredService(); - ControlFactory = ServiceProvider.GetRequiredService(); } private static ServiceProvider BuildServiceProvider() { - var serviceCollection = new ServiceCollection(); - var serviceProvider = ConfigureServices(serviceCollection); - - return serviceProvider; - } - - private static ServiceProvider ConfigureServices(IServiceCollection services) - { - services.AddPowerAppsPersistence(useDefaultTemplates: true); - + var services = new ServiceCollection(); + ConfigureServices(services); return services.BuildServiceProvider(); } - public static IYamlDeserializer CreateDeserializer(bool isControlIdentifiers = false, bool isTextFirst = false) + private static void ConfigureServices(IServiceCollection services) { - return ServiceProvider.GetRequiredService().CreateDeserializer - ( - new YamlSerializationOptions - { - IsTextFirst = isTextFirst, - IsControlIdentifiers = isControlIdentifiers - } - ); - } - - public static IYamlSerializer CreateSerializer(bool isControlIdentifiers = false, bool isTextFirst = false) - { - return ServiceProvider.GetRequiredService().CreateSerializer - ( - new YamlSerializationOptions - { - IsTextFirst = isTextFirst, - IsControlIdentifiers = isControlIdentifiers - } - ); + services.AddMsappArchiveFactory(); } public static string GetTestFilePath(string path, bool isControlIdentifiers = false) { return string.Format(CultureInfo.InvariantCulture, path, isControlIdentifiers ? "-CI" : string.Empty); } - - public static IEnumerable ComponentCustomProperties_Data => new List() - { - new object[] - { - new CustomProperty[] { - new () { Name = "MyTextProp1", DataType = "String", Default = "lorem", - Direction = CustomProperty.PropertyDirection.Input, Type = CustomProperty.PropertyType.Data - } - }, - @"_TestData/ValidYaml{0}/ComponentDefinitions/CustomProperty1.pa.yaml", false - }, - new object[] - { - new CustomProperty[] { - new () { Name = "MyTextProp1", DataType = "String", Default = "lorem", - Direction = CustomProperty.PropertyDirection.Input, Type = CustomProperty.PropertyType.Data - } - }, - @"_TestData/ValidYaml{0}/ComponentDefinitions/CustomProperty1.pa.yaml", true - }, - new object[] - { - new CustomProperty[] { - new () { Name = "MyFuncProp1", DataType = "String", Default = "lorem", - Direction = CustomProperty.PropertyDirection.Input, Type = CustomProperty.PropertyType.Function, - Parameters = new[] { - new CustomPropertyParameter(){ - Name = "param1", - DataType = "String", - IsRequired = true, - } - }, - } - }, - @"_TestData/ValidYaml{0}/ComponentDefinitions/CustomProperty2.pa.yaml", false - }, - new object[] - { - new CustomProperty[] { - new () { Name = "MyFuncProp1", DataType = "String", Default = "lorem", - Direction = CustomProperty.PropertyDirection.Input, Type = CustomProperty.PropertyType.Function, - Parameters = new[] { - new CustomPropertyParameter(){ - Name = "param1", - DataType = "String", - IsRequired = true, - } - }, - } - }, - @"_TestData/ValidYaml{0}/ComponentDefinitions/CustomProperty2.pa.yaml", true - }, - new object[] - { - new CustomProperty[] { - new () { Name = "MyTextProp1", DataType = "String", Default = "lorem", - Direction = CustomProperty.PropertyDirection.Input, Type = CustomProperty.PropertyType.Data - }, - new () { Name = "MyTextProp2", DataType = "String", Default = "lorem", - Direction = CustomProperty.PropertyDirection.Input, Type = CustomProperty.PropertyType.Data - } - }, - @"_TestData/ValidYaml{0}/ComponentDefinitions/with-two-properties.pa.yaml", false - }, - new object[] - { - new CustomProperty[] { - new () { Name = "MyTextProp1", DataType = "String", Default = "lorem", - Direction = CustomProperty.PropertyDirection.Input, Type = CustomProperty.PropertyType.Data - }, - new () { Name = "MyTextProp2", DataType = "String", Default = "lorem", - Direction = CustomProperty.PropertyDirection.Input, Type = CustomProperty.PropertyType.Data - } - }, - @"_TestData/ValidYaml{0}/ComponentDefinitions/with-two-properties.pa.yaml", true - }, - - }; - } diff --git a/src/Persistence.Tests/Yaml/DeserializeComponentDefinitionTests.cs b/src/Persistence.Tests/Yaml/DeserializeComponentDefinitionTests.cs deleted file mode 100644 index d4c8dbea..00000000 --- a/src/Persistence.Tests/Yaml/DeserializeComponentDefinitionTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Persistence.Tests.Yaml; - -[TestClass] -public class DeserializeComponentDefinitionTests : TestBase -{ - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/ComponentDefinitions/with-custom-properties.pa.yaml", true, 6, 1, 3, - "inputFuncImage", 1, "reqColorParam", "a required color param")] - public void Deserialize_Component_Should_Succeed(string path, bool isControlIdentifiers, - int customPropertiesCount, int childrenCount, int propertiesCount, - string firstCustomProperyName, int firstCustomProperyParametersCount, - string firstCustomProperyParameterName, string firstCustomProperyParameterDescription) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var component = deserializer.Deserialize(yamlReader) as ComponentDefinition - ?? throw new InvalidOperationException("Failed to deserialize component"); - - // Assert - component.Should().NotBeNull(); - component.Children!.Count.Should().Be(childrenCount); - component.Properties!.Count.Should().Be(propertiesCount); - component.Template.Should().NotBeNull(); - component.Template!.Name.Should().Be("Component1"); - component.CustomProperties.Count.Should().Be(customPropertiesCount); - component.CustomProperties[0].Name.Should().Be(firstCustomProperyName); - component.CustomProperties[0].Parameters.Count.Should().Be(firstCustomProperyParametersCount); - component.CustomProperties[0].Parameters[0].Name.Should().Be(firstCustomProperyParameterName); - component.CustomProperties[0].Parameters[0].Description.Should().Be(firstCustomProperyParameterDescription); - } - - [TestMethod] - public void Deserialize_CommandComponentDefinition_Should_Succeed() - { - // Arrange - var yaml = "Component1:\n Control: Component\n Type: Command"; - var deserializer = CreateDeserializer(true); - using var reader = new StringReader(yaml); - - // Act - var component = deserializer.Deserialize(reader) as ComponentDefinition; - - // Assert - component.Should().NotBeNull(); - component!.Name.Should().Be("Component1"); - component!.Type.Should().Be(ComponentType.Command); - } -} diff --git a/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs b/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs deleted file mode 100644 index b1657406..00000000 --- a/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using YamlDotNet.Core; - -namespace Persistence.Tests.Yaml; - -[TestClass] -public class DeserializerInvalidTests : TestBase -{ - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void Deserialize_ShouldFailWhenYamlIsInvalid(bool isControlIdentifiers) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - - var files = Directory.GetFiles(GetTestFilePath(@"_TestData/InvalidYaml{0}", isControlIdentifiers), $"*.pa.yaml", SearchOption.AllDirectories); - // Uncomment to test single file - // var files = new string[] { @"_TestData/InvalidYaml/Screen-with-host.pa.yaml" }; - - foreach (var filePath in files) - { - var yaml = File.ReadAllText(filePath); - using var yamlReader = new StringReader(yaml); - var act = () => deserializer.Deserialize(yamlReader); - act.Should().ThrowExactly("deserializing file '{0}' is expected to be invalid", filePath) - .WithErrorCode(PersistenceErrorCode.DeserializationError) - .WithInnerExceptionExactly(); - } - } - - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void Deserialize_ShouldFailWhenExpectingDifferentType(bool isControlIdentifiers) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath("_TestData/ValidYaml{0}/Screen/with-name.pa.yaml", isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - // Explicitly using the wrong type BuiltInControl - Action act = () => { deserializer.Deserialize(yamlReader); }; - - // Assert - act.Should().Throw() - .WithErrorCode(PersistenceErrorCode.DeserializationError) - .WithInnerExceptionExactly() - .WithInnerException() - .WithMessage("Cannot covert Screen to BuiltInControl"); - } - - public record TestSchema - { - public required Screen[] Screens { get; init; } - } - - [TestMethod] - [DataRow(@"_TestData/InvalidYaml{0}/screens-with-duplicates.pa.yaml", false)] - [DataRow(@"_TestData/InvalidYaml{0}/screens-with-duplicates.pa.yaml", true)] - public void Deserialize_Screens_List(string path, bool isControlIdentifiers) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - Action act = () => - { - // Explicitly using list of screens - var result = deserializer.Deserialize(yamlReader); - }; - - // Assert - act.Should().Throw() - .WithErrorCode(PersistenceErrorCode.DeserializationError) - .WithReason("Duplicate control property*") - .WithInnerExceptionExactly(); - } -} diff --git a/src/Persistence.Tests/Yaml/DeserializerValidAppTests.cs b/src/Persistence.Tests/Yaml/DeserializerValidAppTests.cs deleted file mode 100644 index c62166d8..00000000 --- a/src/Persistence.Tests/Yaml/DeserializerValidAppTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Persistence.Tests.Yaml; - -[TestClass] -public class DeserializerValidAppTests : TestBase -{ - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/App/with-screens.pa.yaml", true, typeof(App), "http://microsoft.com/appmagic/appinfo", "App", 2, 0, 1)] - [DataRow(@"_TestData/ValidYaml{0}/App/with-screens.pa.yaml", false, typeof(App), "http://microsoft.com/appmagic/appinfo", "App", 2, 0, 1)] - public void Deserialize_App_Should_Succeed(string path, bool isControlIdentifiers, Type expectedType, string expectedTemplateId, - string expectedName, int screenCount, int controlCount, int propertiesCount) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var app = deserializer.Deserialize(yamlReader); - - // Assert - app.Should().BeAssignableTo(expectedType); - app!.TemplateId.Should().NotBeNull().And.Be(expectedTemplateId); - app!.Name.Should().NotBeNull().And.Be(expectedName); - app!.Screens.Should().NotBeNull().And.HaveCount(screenCount); - if (controlCount > 0) - app.Children.Should().NotBeNull().And.HaveCount(controlCount); - else - app.Children.Should().BeNull(); - app.Properties.Should().NotBeNull().And.HaveCount(propertiesCount); - } -} diff --git a/src/Persistence.Tests/Yaml/DeserializerValidTests.cs b/src/Persistence.Tests/Yaml/DeserializerValidTests.cs deleted file mode 100644 index a3e3e679..00000000 --- a/src/Persistence.Tests/Yaml/DeserializerValidTests.cs +++ /dev/null @@ -1,540 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -namespace Persistence.Tests.Yaml; - -[TestClass] -public class DeserializerValidTests : TestBase -{ - [TestMethod] - [DataRow("NULL")] - [DataRow("~")] - [DataRow("")] - [DataRow(" ")] - [DataRow("# a comment only")] - [DataRow("---")] - public void Deserialize_ReturnsNullInSomeCases(string yaml) - { - var deserializer = ServiceProvider.GetRequiredService().CreateDeserializer(); - - deserializer.Deserialize>(yaml).Should().BeNull(); - } - - [TestMethod] - [DataRow(false, "I am a screen with spaces", "42", "71")] - [DataRow(true, "\"I am a screen with spaces\"", "42", "71")] - [DataRow(true, "NoSpaces", "-50", "=70")] - [DataRow(true, "Yaml : | > ", "", " ")] - [DataRow(true, "Text`~!@#$%^&*()_-+=", ":", "\"\"")] - [DataRow(true, "Text[]{};':\",.<>?/\\|", "@", "")] - [DataRow(false, "こんにちは", "#", "'")] - [DataRow(true, "Cos'è questo?", "---", "33")] - public void Deserialize_ShouldParseSimpleStructure(bool isTextFirst, - string textValue, string xValue, string yValue) - { - var graph = ControlFactory.CreateScreen("Screen1", - properties: new() - { - { "Text", textValue }, - { "X", xValue }, - { "Y", yValue }, - } - ); - - var serializer = ServiceProvider.GetRequiredService().CreateSerializer(new() { IsTextFirst = isTextFirst }); - var yaml = serializer.SerializeControl(graph); - - var deserializer = ServiceProvider.GetRequiredService().CreateDeserializer(new() { IsTextFirst = true }); - - var sut = deserializer.Deserialize(yaml); - sut.ShouldNotBeNull(); - sut.Should().BeOfType(); - sut.Name.Should().Be("Screen1"); - sut.TemplateId.Should().Be("http://microsoft.com/appmagic/screen"); - sut.Children.Should().BeNull(); - sut.Properties.Should().NotBeNull() - .And.HaveCount(3) - .And.ContainKeys("Text", "X", "Y"); - sut.Properties["Text"].Value.Should().Be(textValue); - sut.Properties["X"].Value.Should().Be(xValue); - sut.Properties["Y"].Value.Should().Be(yValue); - } - - [TestMethod] - public void Deserialize_ShouldParseYamlWithChildNodes() - { - var graph = ControlFactory.CreateScreen("Screen1", - properties: new() - { - { "Text", "\"I am a screen\"" }, - }, - children: new Control[] - { - ControlFactory.Create("Label1", template: "text", - properties: - new () - { - { "Text", "\"lorem ipsum\"" }, - }), - ControlFactory.Create("Button1", template: "button", - properties : new () - { - { "Text", "\"click me\"" }, - { "X", "100" }, - { "Y", "200" } - }) - } - ); - - var serializer = ServiceProvider.GetRequiredService().CreateSerializer(); - var yaml = serializer.SerializeControl(graph); - - var deserializer = ServiceProvider.GetRequiredService().CreateDeserializer(); - - var sut = deserializer.Deserialize(yaml); - sut.ShouldNotBeNull(); - sut.Should().BeOfType(); - sut.Name.Should().Be("Screen1"); - sut.TemplateId.Should().Be("http://microsoft.com/appmagic/screen"); - sut.Properties.Should().NotBeNull() - .And.HaveCount(1) - .And.ContainKey("Text"); - sut.Properties["Text"].Value.Should().Be("\"I am a screen\""); - - sut.Children.Should().NotBeNull().And.HaveCount(2); - sut.Children![0].Should().BeOfType(); - sut.Children![0].Name.Should().Be("Label1"); - sut.Children![0].TemplateId.Should().Be("http://microsoft.com/appmagic/text"); - sut.Children![0].Properties.Should().NotBeNull() - .And.HaveCount(2) - .And.ContainKeys("Text", PropertyNames.ZIndex); - sut.Children![0].Properties["Text"].Value.Should().Be("\"lorem ipsum\""); - sut.Children![0].Properties[PropertyNames.ZIndex].Value.Should().Be("2"); - - sut.Children![1].Should().BeOfType(); - sut.Children![1].Name.Should().Be("Button1"); - sut.Children![1].TemplateId.Should().Be("http://microsoft.com/appmagic/button"); - sut.Children![1].Properties.Should().NotBeNull() - .And.HaveCount(4) - .And.ContainKeys("Text", "X", "Y", PropertyNames.ZIndex); - sut.Children![1].Properties["Text"].Value.Should().Be("\"click me\""); - sut.Children![1].Properties["X"].Value.Should().Be("100"); - sut.Children![1].Properties["Y"].Value.Should().Be("200"); - sut.Children![1].Properties[PropertyNames.ZIndex].Value.Should().Be("1"); - } - - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void Deserialize_GroupContainersShouldOrderZIndexInverse(bool isControlIdentifiers) - { - var graph = ControlFactory.CreateScreen("Screen1", - properties: new() - { - { "Text", "\"I am a screen\"" }, - }, - children: new Control[] - { - ControlFactory.Create("group", template: "groupContainer", children: new Control[] { - ControlFactory.Create("Label1", template: "text", - properties: - new () - { - { "Text", "\"lorem ipsum\"" }, - }), - ControlFactory.Create("Button1", template: "button", - properties : new () - { - { "Text", "\"click me\"" }, - { "X", "100" }, - { "Y", "200" } - }) - }) - } - ); - - var serializer = CreateSerializer(isControlIdentifiers); - var yaml = serializer.SerializeControl(graph); - - var deserializer = CreateDeserializer(isControlIdentifiers); - - var sut = deserializer.Deserialize(yaml); - sut.ShouldNotBeNull(); - sut.Should().BeOfType(); - sut.Name.Should().Be("Screen1"); - sut.TemplateId.Should().Be("http://microsoft.com/appmagic/screen"); - sut.Properties.Should().NotBeNull() - .And.HaveCount(1) - .And.ContainKey("Text"); - sut.Properties["Text"].Value.Should().Be("\"I am a screen\""); - sut.Children.Should().NotBeNull().And.HaveCount(1); - - var group = sut.Children![0]; - - group.Children.Should().NotBeNull().And.HaveCount(2); - group.Children![0].Should().BeOfType(); - group.Children![0].Name.Should().Be("Label1"); - group.Children![0].TemplateId.Should().Be("http://microsoft.com/appmagic/text"); - group.Children![0].Properties.Should().NotBeNull() - .And.HaveCount(2) - .And.ContainKeys("Text", PropertyNames.ZIndex); - group.Children![0].Properties["Text"].Value.Should().Be("\"lorem ipsum\""); - group.Children![0].Properties[PropertyNames.ZIndex].Value.Should().Be("1"); - - group.Children![1].Should().BeOfType(); - group.Children![1].Name.Should().Be("Button1"); - group.Children![1].TemplateId.Should().Be("http://microsoft.com/appmagic/button"); - group.Children![1].Properties.Should().NotBeNull() - .And.HaveCount(4) - .And.ContainKeys("Text", "X", "Y", PropertyNames.ZIndex); - group.Children![1].Properties["Text"].Value.Should().Be("\"click me\""); - group.Children![1].Properties["X"].Value.Should().Be("100"); - group.Children![1].Properties[PropertyNames.ZIndex].Value.Should().Be("2"); - } - - [TestMethod] - public void Deserialize_ShouldParseYamlForCustomControl() - { - var graph = ControlFactory.Create("CustomControl1", template: "http://localhost/#customcontrol", - properties: new() - { - { "Text", "\"I am a custom control\"" }, - } - ); - - var serializer = ServiceProvider.GetRequiredService().CreateSerializer(); - var yaml = serializer.SerializeControl(graph); - - var deserializer = ServiceProvider.GetRequiredService().CreateDeserializer(); - - var sut = deserializer.Deserialize(yaml); - sut.ShouldNotBeNull(); - sut.Should().BeOfType(); - sut.Name.Should().Be("CustomControl1"); - sut.TemplateId.Should().Be("http://localhost/#customcontrol"); - sut.Children.Should().BeNull(); - sut.Properties.Should().NotBeNull() - .And.HaveCount(1) - .And.ContainKey("Text"); - sut.Properties["Text"].Value.Should().Be("\"I am a custom control\""); - } - - [TestMethod] - [DataRow("ButtonCanvas", "BuiltIn Button")] - [DataRow("TextCanvas", "Text control name")] - public void Deserialize_ShouldParseBuiltInControlFromYamlCustomControl(string templateName, string controlName) - { - var graph = ControlFactory.Create(controlName, templateName); - - var serializer = ServiceProvider.GetRequiredService().CreateSerializer(); - var yaml = serializer.SerializeControl(graph); - - var deserializer = ServiceProvider.GetRequiredService().CreateDeserializer(); - - var sut = deserializer.Deserialize(yaml); - sut.Should().NotBeNull().And.BeOfType(); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-controls.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", "Screen 1", 2, 2)] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-controls.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", "Screen 1", 2, 2)] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-name.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", "My Power Apps Screen", 0, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-name.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", "My Power Apps Screen", 0, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Control-with-custom-template.pa.yaml", true, typeof(CustomControl), "http://localhost/#customcontrol", "My Power Apps Custom Control", 0, 8)] - [DataRow(@"_TestData/ValidYaml{0}/Control-with-custom-template.pa.yaml", false, typeof(CustomControl), "http://localhost/#customcontrol", "My Power Apps Custom Control", 0, 8)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-template-id.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", "Hello", 0, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-template-id.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", "Hello", 0, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-template-name.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", "Hello", 0, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-template-name.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", "Hello", 0, 0)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-template.pa.yaml", true, typeof(BuiltInControl), "http://microsoft.com/appmagic/button", "button with template", 0, 1)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-template.pa.yaml", false, typeof(BuiltInControl), "http://microsoft.com/appmagic/button", "button with template", 0, 1)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-template-id.pa.yaml", true, typeof(BuiltInControl), "http://microsoft.com/appmagic/button", "button with template id", 0, 1)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-template-id.pa.yaml", false, typeof(BuiltInControl), "http://microsoft.com/appmagic/button", "button with template id", 0, 1)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-template-name.pa.yaml", true, typeof(BuiltInControl), "http://microsoft.com/appmagic/button", "button with template name", 0, 1)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-template-name.pa.yaml", false, typeof(BuiltInControl), "http://microsoft.com/appmagic/button", "button with template name", 0, 1)] - public void Deserialize_ShouldSucceed(string path, bool isControlIdentifiers, Type expectedType, string expectedTemplateId, string expectedName, int controlCount, int propertiesCount) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var controlObj = deserializer.Deserialize(yamlReader); - - // Assert - controlObj.Should().BeAssignableTo(expectedType); - controlObj!.TemplateId.Should().NotBeNull().And.Be(expectedTemplateId); - controlObj!.Name.Should().NotBeNull().And.Be(expectedName); - if (controlCount > 0) - controlObj.Children.Should().NotBeNull().And.HaveCount(controlCount); - else - controlObj.Children.Should().BeNull(); - controlObj.Properties.Should().NotBeNull().And.HaveCount(propertiesCount); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/App.pa.yaml", true, 1, 0)] - [DataRow(@"_TestData/ValidYaml{0}/App.pa.yaml", false, 1, 0)] - public void Deserialize_App_ShouldSucceed(string path, bool isControlIdentifiers, int controlCount, int propertiesCount) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var app = deserializer.Deserialize(yamlReader); - - app!.Name.Should().NotBeNull().And.Be(App.ControlName); - app.Children.Should().NotBeNull().And.HaveCount(controlCount); - app.Properties.Should().NotBeNull().And.HaveCount(propertiesCount); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-unmatched-field.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-unmatched-field.pa.yaml", false)] - public void Deserialize_ShouldIgnoreUnmatchedProperties(string path, bool isControlIdentifiers) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var controlObj = deserializer.Deserialize(yamlReader); - - // Assert - controlObj.Should().NotBeNull(); - } - - [TestMethod] - public void Deserialize_Strings() - { - // Arrange - var deserializer = ServiceProvider.GetRequiredService().CreateDeserializer - ( - new YamlSerializationOptions() { IsTextFirst = true } - ); - using var yamlStream = File.OpenRead(@"_TestData/ValidYaml/Strings.pa.yaml"); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var controlObj = deserializer.Deserialize(yamlReader); - - // Assert - controlObj.ShouldNotBeNull(); - controlObj.Properties.Should().NotBeNull().And.HaveCount(12); - - controlObj.Properties["NormalText"].Value.Should().Be("\"This is a normal text\""); - controlObj.Properties["MultiLineString"].Value.Should().Be("\"This is a multi-line\nstring\""); - controlObj.Properties["NothingString"].Value.Should().NotBeNull().And.Be("\"\""); - controlObj.Properties["NullTilde"].Value.Should().NotBeNull().And.Be("\"\""); - controlObj.Properties["NullAsString"].Value.Should().NotBeNull().And.Be("\"null\""); - controlObj.Properties["NullString"].Value.Should().NotBeNull().And.Be("\"\""); - controlObj.Properties["EmptyString"].Value.Should().NotBeNull().And.Be("\"\""); - controlObj.Properties["WhiteSpaceString"].Value.Should().NotBeNull().And.Be("\" \""); - controlObj.Properties["NormalTextAgain"].Value.Should().Be("\"This is a normal text\""); - controlObj.Properties["StartsWithEquals"].Value.Should().Be("\"=This string starts with equals\""); - controlObj.Properties["StartsWithEqualsMultiLine"].Value.Should().Be("\"=This is a multi-line\nstarts with equals\""); - controlObj.Properties["Formula"].Value.Should().Be("1+1"); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Component.pa.yaml", true, "MyCustomComponent", "http://microsoft.com/appmagic/Component", "lorem ipsum", true)] - [DataRow(@"_TestData/ValidYaml{0}/Component.pa.yaml", false, "MyCustomComponent", "http://microsoft.com/appmagic/Component", "lorem ipsum", true)] - [DataRow(@"_TestData/ValidYaml{0}/ComponentDefinitions/CommandComponent.pa.yaml", true, "MyCustomCommandComponent", "http://microsoft.com/appmagic/CommandComponent", "lorem ipsum", true)] - [DataRow(@"_TestData/ValidYaml{0}/ComponentDefinitions/CommandComponent.pa.yaml", false, "MyCustomCommandComponent", "http://microsoft.com/appmagic/CommandComponent", "lorem ipsum", true)] - public void Deserialize_ComponentDefinition_ShouldSucceed( - string path, bool isControlIdentifiers, - string expectedName, - string expectedTemplateId, - string expectedDescription, - bool expectedAccessAppScope) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var component = deserializer.Deserialize(yamlReader); - - // Assert - component.ShouldNotBeNull(); - component.Name.Should().Be(expectedName); - component.Description.Should().Be(expectedDescription); - component.AccessAppScope.Should().Be(expectedAccessAppScope); - component.Template.Should().NotBeNull(); - component.Template!.Name.Should().Be(expectedName); - component.Template.Id.Should().Be(expectedTemplateId); - } - - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-variant.pa.yaml", true, "built in", "Button", "SuperButton")] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-variant.pa.yaml", false, "built in", "Button", "SuperButton")] - public void Variant_ShouldSucceed( - string path, bool isControlIdentifiers, - string expectedName, - string expectedTemplateName, - string expectedVariant) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var control = deserializer.Deserialize(yamlReader); - - // Assert - control.ShouldNotBeNull(); - control.Name.Should().Be(expectedName); - control.Template.Should().NotBeNull(); - control.Template!.Name.Should().Be(expectedTemplateName); - control.Variant.Should().Be(expectedVariant); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-layout.pa.yaml", true, "built in", "Button", "vertical")] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-layout.pa.yaml", false, "built in", "Button", "vertical")] - public void Layout_ShouldSucceed( - string path, bool isControlIdentifiers, - string expectedName, - string expectedTemplateName, - string expectedLayout) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var control = deserializer.Deserialize(yamlReader); - - // Assert - control.ShouldNotBeNull(); - control.Name.Should().Be(expectedName); - control.Template.Should().NotBeNull(); - control.Template!.Name.Should().Be(expectedTemplateName); - control.Layout.Should().Be(expectedLayout); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-gallery.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-gallery.pa.yaml", false)] - public void Deserialize_Should_AddGalleryTemplate(string path, bool isControlIdentifiers) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var screen = deserializer.Deserialize(yamlReader); - - // Assert - screen.ShouldNotBeNull(); - screen.Children.ShouldNotBeNull(); - screen.Children.Should().NotBeNull().And.HaveCount(1); - var gallery = screen.Children[0]; - gallery.Should().NotBeNull().And.BeOfType(); - gallery.Template.Name.Should().Be("Gallery"); - gallery.Children.ShouldNotBeNull(); - - // Check properties got moved to the gallery template - gallery.Children.Should().HaveCount(2); - gallery.Properties.Should().NotBeNull().And.HaveCount(2); - gallery.Properties.Should().NotContainKeys("TemplateFill", "OnSelect"); - var galleryTemplate = gallery.Children.ElementAt(0); - galleryTemplate.ShouldNotBeNull(); - galleryTemplate.Template.Name.Should().Be("GalleryTemplate"); - galleryTemplate.Properties.Should().NotBeNull().And.HaveCount(1); - galleryTemplate.Properties.Should().ContainKeys("TemplateFill"); - } - - [TestMethod] - [DynamicData(nameof(ComponentCustomProperties_Data), typeof(TestBase))] - public void Deserialize_ShouldParseYamlForComponentDefinitionCustomProperties(CustomProperty[] expectedCustomProperties, string yamlFile, bool isControlIdentifiers) - { - var expectedYaml = File.ReadAllText(GetTestFilePath(yamlFile, isControlIdentifiers)); - var deserializer = CreateDeserializer(isControlIdentifiers); - - var component = deserializer.Deserialize(expectedYaml); - component.ShouldNotBeNull(); - component.CustomProperties.Should().NotBeNull() - .And.HaveCount(expectedCustomProperties.Length); - - for (var i = 0; i < expectedCustomProperties.Length; i++) - { - component.CustomProperties[i].Should().BeEquivalentTo(expectedCustomProperties[i]); - } - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Group/with-two-children.pa.yaml", true, 2, "My Small Group")] - [DataRow(@"_TestData/ValidYaml{0}/Group/with-two-children.pa.yaml", false, 2, "My Small Group")] - [DataRow(@"_TestData/ValidYaml{0}/Group/with-nested-children.pa.yaml", true, 2, "My Nested Group")] - [DataRow(@"_TestData/ValidYaml{0}/Group/with-nested-children.pa.yaml", false, 2, "My Nested Group")] - public void Deserialize_ShouldParseYamlForGroupWithChildren(string path, bool isControlIdentifiers, int expectedChildrenCount, string expectedName) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var group = deserializer.Deserialize(yamlReader); - - // Assert - group.ShouldNotBeNull(); - group.Children.Should().NotBeNull().And.HaveCount(expectedChildrenCount); - group.Name.Should().Be(expectedName); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/With-list-of-controls.pa.yaml", true, 2, typeof(CustomControl), "BuiltIn Control1")] - [DataRow(@"_TestData/ValidYaml{0}/With-list-of-controls.pa.yaml", false, 2, typeof(CustomControl), "BuiltIn Control1")] - public void Deserialize_ShouldParse_Lists_of_Controls(string path, bool isControlIdentifiers, int expectedCount, Type expectedType, string expectedName) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var listObj = deserializer.Deserialize(yamlReader); - listObj.ShouldNotBeNull(); - listObj.Should().BeAssignableTo(typeof(List)); - var list = (List)listObj; - list.Should().HaveCount(expectedCount); - list.First().Should().BeOfType(expectedType); - list.First().Name.Should().Be(expectedName); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-style.pa.yaml", true, "this is my style")] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-style.pa.yaml", false, "this is my style")] - public void Deserialize_ShouldParse_StyleName( - string path, - bool isControlIdentifiers, - string expectedStyleName) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act - var control = deserializer.Deserialize(yamlReader); - - // Assert - control.ShouldNotBeNull(); - control.StyleName.Should().Be(expectedStyleName); - } -} diff --git a/src/Persistence.Tests/Yaml/RoundTripTests.cs b/src/Persistence.Tests/Yaml/RoundTripTests.cs deleted file mode 100644 index c179c02b..00000000 --- a/src/Persistence.Tests/Yaml/RoundTripTests.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Persistence.Tests.Yaml; - -[TestClass] -public class RoundTripTests : TestBase -{ - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-name.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", - "My Power Apps Screen", 0, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-name.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", - "My Power Apps Screen", 0, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-controls.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Screen 1", 2, 2)] - [DataRow(@"_TestData/ValidYaml{0}/Screen-with-controls.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Screen 1", 2, 2)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-two-properties.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Hello", 2, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-two-properties.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Hello", 2, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-properties-and-controls.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Screen with two properties and two controls", 2, 2)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-properties-and-controls.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Screen with two properties and two controls", 2, 2)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-properties-and-nested-controls.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Screen with two properties and two nested controls", 2, 2)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-properties-and-nested-controls.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Screen with two properties and two nested controls", 2, 2)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-multiline-properties.pa.yaml", true, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Screen with two multiline properties", 2, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-multiline-properties.pa.yaml", false, typeof(Screen), "http://microsoft.com/appmagic/screen", - "Screen with two multiline properties", 2, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Control-with-custom-template.pa.yaml", true, typeof(CustomControl), "http://localhost/#customcontrol", - "My Power Apps Custom Control", 8, 0)] - [DataRow(@"_TestData/ValidYaml{0}/Control-with-custom-template.pa.yaml", false, typeof(CustomControl), "http://localhost/#customcontrol", - "My Power Apps Custom Control", 8, 0)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl1.pa.yaml", true, typeof(BuiltInControl), "http://microsoft.com/appmagic/powercontrol/PowerApps_CoreControls_ButtonCanvas", - "BuiltIn Control1", 1, 0)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl1.pa.yaml", false, typeof(BuiltInControl), "http://microsoft.com/appmagic/powercontrol/PowerApps_CoreControls_ButtonCanvas", - "BuiltIn Control1", 1, 0)] - public void RoundTrip_ValidYaml(string path, bool isControlIdentifiers, Type rootType, string expectedTemplateId, string expectedName, int expectedPropsCount, int expectedControlCount) - { - var deserializer = CreateDeserializer(isControlIdentifiers); - var serializer = CreateSerializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Deserialize the yaml into an object. - var controlObj = deserializer.DeserializeControl(yamlReader, rootType); - - // Validate the control. - controlObj.Should().BeAssignableTo(rootType); - var control = (Control)controlObj!; - control.TemplateId.Should().Be(expectedTemplateId); - control.Name.Should().Be(expectedName); - control.Properties.Should().HaveCount(expectedPropsCount); - if (expectedControlCount > 0) - control.Children.Should().HaveCount(expectedControlCount); - else - control.Children.Should().BeNull(); - - // Serialize the object back into yaml. - var actualYaml = serializer.SerializeControl(control).NormalizeNewlines(); - - // Assert that the yaml is the same. - var expectedYaml = File.ReadAllText(GetTestFilePath(path, isControlIdentifiers)).NormalizeNewlines(); - actualYaml.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-component-instance.pa.yaml", true, "MyComponentLibrary")] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-component-instance.pa.yaml", false, "MyComponentLibrary")] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-component-instance-no-library.pa.yaml", true, "")] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-component-instance-no-library.pa.yaml", false, "")] - public void Screen_With_Component_Instance(string path, bool isControlIdentifiers, string componentLibraryName) - { - // Arrange - var deserializer = CreateDeserializer(isControlIdentifiers); - using var yamlStream = File.OpenRead(GetTestFilePath(path, isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - // Act I: Deserialize the yaml into an object. - var screen = deserializer.Deserialize(yamlReader) as Screen - ?? throw new InvalidOperationException("Failed to deserialize screen"); - - // Assert - screen.Children!.Count.Should().Be(1); - var customControl = screen.Children[0] as ComponentInstance - ?? throw new InvalidOperationException("Failed to deserialize component instance"); - customControl.Should().NotBeNull(); - customControl.Name.Should().Be("This is custom component"); - customControl.ComponentName.Should().Be("ComponentDefinition_1"); - customControl.ComponentLibraryUniqueName.Should().Be(componentLibraryName); - - // Act II: Serialize the object back into yaml. - var serializer = CreateSerializer(isControlIdentifiers); - var actualYaml = serializer.SerializeControl(screen).NormalizeNewlines(); - - // Assert that the yaml is the same. - var expectedYaml = File.ReadAllText(GetTestFilePath(path, isControlIdentifiers)).NormalizeNewlines(); - actualYaml.Should().Be(expectedYaml); - } -} diff --git a/src/Persistence.Tests/Yaml/ValidSerializerTests.cs b/src/Persistence.Tests/Yaml/ValidSerializerTests.cs deleted file mode 100644 index 4b5c0c1a..00000000 --- a/src/Persistence.Tests/Yaml/ValidSerializerTests.cs +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Persistence.Tests.Yaml; - -[TestClass] -public class ValidSerializerTests : TestBase -{ - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-name.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-name.pa.yaml", false)] - public void Serialize_ShouldCreateValidYaml_for_Screen(string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.Create("Hello", "Screen"); - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/App.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/App.pa.yaml", false)] - public void Serialize_ShouldCreateValidYamlForApp(string expectedPath, bool isControlIdentifiers) - { - var app = ControlFactory.CreateApp(); - - app.Screens = new Screen[] - { - ControlFactory.CreateScreen("Screen1", - properties: new() - { - { "Text", "\"I am a screen\"" }, - }), - ControlFactory.CreateScreen("Screen2", - properties: new() - { - { "Text", "\"I am another screen\"" }, - }), - }; - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(app).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-properties-not-sorted.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-properties-not-sorted.pa.yaml", false)] - public void Serialize_ShouldSortControlPropertiesAlphabetically(string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.CreateScreen("Screen1", - properties: new() - { - { "PropertyB", "=B" }, - { "PropertyC", "=C" }, - { "PropertyA", "=A" }, - }); - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-properties-and-controls1.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-properties-and-controls1.pa.yaml", false)] - public void Serialize_ShouldCreateValidYamlWithChildNodes(string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.Create("Screen1", "Screen", - properties: new() - { - { "Text", "\"I am a screen\"" }, - }, - children: new Control[] - { - ControlFactory.Create("Label1", template: "text", - properties: new() - { - { "Text", "\"lorem ipsum\"" }, - } - ), - ControlFactory.Create("Button1", template: "button", - properties: new() - { - { "Text", "\"click me\"" }, - { "X", "100" }, - { "Y", "200" } - } - ) - } - ); - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/CustomControls/with-property.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/CustomControls/with-property.pa.yaml", false)] - public void Serialize_ShouldCreateValidYamlForCustomControl(string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.Create("CustomControl1", template: "http://localhost/#customcontrol", - properties: new() - { - { "Text", "\"I am a custom control\"" } - } - ); - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow("ButtonCanvas", "$\"Interpolated text {User().FullName}\"", @"_TestData/ValidYaml{0}/BuiltInControl1.pa.yaml", false)] - [DataRow("ButtonCanvas", "$\"Interpolated text {User().FullName}\"", @"_TestData/ValidYaml{0}/BuiltInControl1.pa.yaml", true)] - [DataRow("ButtonCanvas", "\"Normal text\"", @"_TestData/ValidYaml{0}/BuiltInControl2.pa.yaml", false)] - [DataRow("ButtonCanvas", "\"Normal text\"", @"_TestData/ValidYaml{0}/BuiltInControl2.pa.yaml", true)] - [DataRow("ButtonCanvas", "\"Text`~!@#$%^&*()_-+=\", \":\"", @"_TestData/ValidYaml{0}/BuiltInControl3.pa.yaml", false)] - [DataRow("ButtonCanvas", "\"Text`~!@#$%^&*()_-+=\", \":\"", @"_TestData/ValidYaml{0}/BuiltInControl3.pa.yaml", true)] - [DataRow("ButtonCanvas", "\"Hello : World\"", @"_TestData/ValidYaml{0}/BuiltInControl4.pa.yaml", false)] - [DataRow("ButtonCanvas", "\"Hello : World\"", @"_TestData/ValidYaml{0}/BuiltInControl4.pa.yaml", true)] - [DataRow("ButtonCanvas", "\"Hello # World\"", @"_TestData/ValidYaml{0}/BuiltInControl5.pa.yaml", false)] - [DataRow("ButtonCanvas", "\"Hello # World\"", @"_TestData/ValidYaml{0}/BuiltInControl5.pa.yaml", true)] - [DataRow("ButtonCanvas", "'Hello single quoted'.Text", @"_TestData/ValidYaml{0}/BuiltInControl6.pa.yaml", false)] - [DataRow("ButtonCanvas", "'Hello single quoted'.Text", @"_TestData/ValidYaml{0}/BuiltInControl6.pa.yaml", true)] - [DataRow("ButtonCanvas", "\"=Starts with equals\"", @"_TestData/ValidYaml{0}/BuiltInControl7.pa.yaml", false)] - [DataRow("ButtonCanvas", "\"=Starts with equals\"", @"_TestData/ValidYaml{0}/BuiltInControl7.pa.yaml", true)] - [DataRow("ButtonCanvas", "\"Text containing PFX \"\"Double-Double-Quote\"\" escape sequence\"", @"_TestData/ValidYaml{0}/BuiltInControl8.pa.yaml", false)] - [DataRow("ButtonCanvas", "\"Text containing PFX \"\"Double-Double-Quote\"\" escape sequence\"", @"_TestData/ValidYaml{0}/BuiltInControl8.pa.yaml", true)] - public void Serialize_ShouldCreateValidYaml_ForBuiltInControl(string templateName, string controlText, string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.Create("BuiltIn Control1", template: templateName, - properties: new() - { - { "Text", controlText } - } - ); - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-gallery.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-gallery.pa.yaml", false)] - public void Serialize_Should_FlattenGalleryTemplate(string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.CreateScreen("Screen1", - properties: new() - { - { "Text", "\"I am a screen\"" }, - }, - children: new Control[] - { - ControlFactory.Create("Gallery1", template: "gallery", - properties: new() - { - { "Items", "Accounts" }, - }, - children: new List() - { - ControlFactory.Create("galleryTemplate1", template: "galleryTemplate", - properties: new() - { - { "TemplateFill", "RGBA(0, 0, 0, 0)" }, - { "ZIndex", "1" }, - } - ), - ControlFactory.Create("button12", template: "button", - properties: new() - { - { "Fill", "RGBA(0, 0, 0, 0)" }, - { "ZIndex", "1" }, - } - ) - } - ) - } - ); - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/ZIndexOrdering/with-addpropertiestoparents-control.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/ZIndexOrdering/with-addpropertiestoparents-control.pa.yaml", false)] - public void Serialize_Should_FlattenGalleryTemplateWithoutZIndex(string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.Create("Gallery1", template: "gallery", - properties: new() - { - { "Items", "Accounts" }, - }, - children: new List() - { - ControlFactory.Create("GalleryTemplate1", template: "galleryTemplate", - properties: new() - { - { "TemplateFill", "RGBA(0, 0, 0, 0)" }, - { "ZIndex", "1" }, - } - ), - ControlFactory.Create("Button1", template: "button", - properties: new() - { - { "Fill", "RGBA(0, 0, 0, 0)" }, - { "ZIndex", "1" }, - } - ) - } - ); - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-variant.pa.yaml", "SuperButton", null, true)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-variant.pa.yaml", "SuperButton", null, false)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-layout.pa.yaml", "SuperButton", "vertical", true)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-layout.pa.yaml", "SuperButton", "vertical", false)] - public void Valid_Variant(string expectedPath, string expectedVariant, string expectedLayout, bool isControlIdentifiers) - { - var graph = ControlFactory.Create("built in", "Button", variant: expectedVariant, - properties: new() - { - { "Text", "\"button text\"" }, - } - ); - graph.Layout = expectedLayout; - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-sorted-properties.pa.yaml", true, "SuperButton", "Some value")] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-sorted-properties.pa.yaml", false, "SuperButton", "Some value")] - public void Should_SortBy_Category(string expectedPath, bool isControlIdentifiers, string templateName, string expectedValue) - { - var graph = ControlFactory.Create("BuiltIn Control1", template: templateName, - properties: new Dictionary() - { - { "a2_Data", new("a2_Data", expectedValue) { Category = PropertyCategory.Data } }, - { "a1_Data", new("a1_Data", expectedValue) { Category = PropertyCategory.Data } }, - { "x2_Behavior", new("x2_Behavior", expectedValue) { Category = PropertyCategory.Behavior } }, - { "x1_Behavior", new("x1_Behavior", expectedValue) { Category = PropertyCategory.Behavior } }, - { "y3_Design", new("y3_Design", expectedValue) { Category = PropertyCategory.Design } }, - { "y2_Design", new("y2_Design", expectedValue) { Category = PropertyCategory.Design } }, - { "y1_Design", new("y1_Design", expectedValue) { Category = PropertyCategory.Design } }, - } - ); - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/With-list-of-controls.pa.yaml", true, "SuperButton")] - [DataRow(@"_TestData/ValidYaml{0}/With-list-of-controls.pa.yaml", false, "SuperButton")] - public void Should_Serialize_List_Of_Controls(string expectedPath, bool isControlIdentifiers, string templateName) - { - var graph = new List() - { - ControlFactory.Create("BuiltIn Control1", template: templateName, - properties: new() - { - { "Text", "Just text" }, - } - ), - ControlFactory.Create("BuiltIn Label", template: "Label", - properties: new() - { - { "Text", "Just label text" }, - } - ), - }; - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.Serialize(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } - - [TestMethod] - [DynamicData(nameof(ComponentCustomProperties_Data), typeof(TestBase))] - public void Serialize_ShouldCreateValidYamlForComponentCustomProperties(CustomProperty[] customProperties, string expectedYamlFile, bool isControlIdentifiers) - { - var component = ControlFactory.Create("Component1", "Component") as ComponentDefinition; - component.Should().NotBeNull(); - component!.CustomProperties.Should().NotBeNull(); - foreach (var prop in customProperties) - { - component.CustomProperties.Add(prop); - } - - var sut = CreateSerializer(isControlIdentifiers); - - var serializedComponent = sut.SerializeControl(component).NormalizeNewlines(); - - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedYamlFile, isControlIdentifiers)).NormalizeNewlines(); - serializedComponent.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(ComponentType.Canvas, "lorem ipsum dolor", false, "Control: Component\nName: Component1\nDescription: lorem ipsum dolor\n")] - [DataRow(ComponentType.Canvas, "lorem ipsum dolor", true, "Control: Component\nName: Component1\nDescription: lorem ipsum dolor\nAccessAppScope: true\n")] - [DataRow(ComponentType.Canvas, "", true, "Control: Component\nName: Component1\nAccessAppScope: true\n")] - [DataRow(ComponentType.Command, "", true, "Control: Component\nName: Component1\nType: Command\nAccessAppScope: true\n")] - public void Serialize_ShouldCreateValidYamlForComponentDefinitions(ComponentType componentType, string description, bool accessAppScope, string expectedYaml) - { - var name = "Component1"; - var template = ControlFactory.CreateComponentTemplate(name, componentType); - var component = (ComponentDefinition)ControlFactory.Create(name, template); - component.Should().NotBeNull(); - component.Type = componentType; - component.Description = description; - component.AccessAppScope = accessAppScope; - - var sut = CreateSerializer(); - - var serializedComponent = sut.SerializeControl(component).NormalizeNewlines(); - - serializedComponent.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-component-instance.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/Screen/with-component-instance.pa.yaml", false)] - public void Serialize_ShouldCreateValidYamlForComponentInstance(string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.CreateScreen("Hello", - properties: new(), - children: new Control[] - { - new ComponentInstance() - { - Name = "This is custom component", - ComponentName = "ComponentDefinition_1", - ComponentLibraryUniqueName = "MyComponentLibrary", - Variant = string.Empty, - Template = new Microsoft.PowerPlatform.PowerApps.Persistence.Templates.ControlTemplate("http://localhost/Component"), - Properties = new() - { - new ControlProperty("X", "15"), - new ControlProperty("Y", "55"), - } - } - }); - var sut = CreateSerializer(isControlIdentifiers: isControlIdentifiers); - - var serializedGraph = sut.SerializeControl(graph).NormalizeNewlines(); - - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - serializedGraph.Should().Be(expectedYaml); - } - - [TestMethod] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-style.pa.yaml", true)] - [DataRow(@"_TestData/ValidYaml{0}/BuiltInControl/with-style.pa.yaml", false)] - public void Serialize_ShouldWriteStyle(string expectedPath, bool isControlIdentifiers) - { - var graph = ControlFactory.Create("myControl1", template: "Button"); - graph.StyleName = "this is my style"; - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph).NormalizeNewlines(); - var expectedYaml = File.ReadAllText(GetTestFilePath(expectedPath, isControlIdentifiers)).NormalizeNewlines(); - sut.Should().Be(expectedYaml); - } -} diff --git a/src/Persistence.Tests/Yaml/ZIndexOrderingTests.cs b/src/Persistence.Tests/Yaml/ZIndexOrderingTests.cs deleted file mode 100644 index 69f0109e..00000000 --- a/src/Persistence.Tests/Yaml/ZIndexOrderingTests.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Globalization; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -namespace Persistence.Tests.Yaml; - -[TestClass] -public class ZIndexOrderingTests : TestBase -{ - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void SerializedChildrenInZOrder(bool isControlIdentifiers) - { - var graph = ControlFactory.CreateScreen("Screen1", - properties: new() { { "Text", "\"I am a screen\"" }, }, - children: new Control[] - { - MakeLabelWithZIndex(1), - MakeLabelWithZIndex(3), - MakeLabelWithZIndex(2), - MakeLabelWithZIndex(5), - MakeLabelWithZIndex(4), - } - ); - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.SerializeControl(graph); - var expected = File.ReadAllText(GetTestFilePath(@"_TestData/ValidYaml{0}/ZIndexOrdering/Screen-with-sorted-children.pa.yaml", isControlIdentifiers)); - sut.Should().BeEquivalentTo(expected); - } - - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void DeserializeChildrenShouldHaveMatchingZIndexProperty(bool isControlIdentifiers) - { - var deserializer = CreateDeserializer(isControlIdentifiers); - - using var yamlStream = File.OpenRead(GetTestFilePath(@"_TestData/ValidYaml{0}/ZIndexOrdering/Screen-with-sorted-children.pa.yaml", isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - var sut = deserializer.Deserialize(yamlReader); - - sut.ShouldNotBeNull(); - sut.Children.Should() - .HaveCount(5) - .And.BeEquivalentTo(new List - { - MakeLabelWithZIndex(5), - MakeLabelWithZIndex(4), - MakeLabelWithZIndex(3), - MakeLabelWithZIndex(2), - MakeLabelWithZIndex(1), - }); - } - - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void Deserialize_List_Should_Be_With_ZIndex(bool isControlIdentifiers) - { - var deserializer = CreateDeserializer(isControlIdentifiers); - - using var yamlStream = File.OpenRead(GetTestFilePath(@"_TestData/ValidYaml{0}/ZIndexOrdering/With-list-of-controls-with-children.pa.yaml", isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - var result = deserializer.Deserialize>(yamlReader); - - result.ShouldNotBeNull(); - result.Should().HaveCount(3); - for (var i = 0; i < result.Count; i++) - { - result[i].Name.Should().Be($"Group{i + 1}"); - result[i].Children.Should().NotBeNull(); - for (var j = 0; j < result[i].Children!.Count; j++) - { - result[i].Children![j].Name.Should().Be($"Label{j + 1}"); - result[i].Children![j].ZIndex.Should().Be(result[i].Children!.Count - j); - } - } - } - - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void Deserialize_Container_List_Should_Be_With_ZIndex_Inverse(bool isControlIdentifiers) - { - var deserializer = CreateDeserializer(isControlIdentifiers); - - using var yamlStream = File.OpenRead(GetTestFilePath(@"_TestData/ValidYaml{0}/ZIndexOrdering/With-list-of-container-controls-with-children.pa.yaml", isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - var result = deserializer.Deserialize>(yamlReader); - - result.ShouldNotBeNull(); - result.Should().HaveCount(3); - for (var i = 0; i < result.Count; i++) - { - result[i].Name.Should().Be($"Group{i + 1}"); - result[i].Children.Should().NotBeNull(); - for (var j = 0; j < result[i].Children!.Count; j++) - { - result[i].Children![j].Name.Should().Be($"Label{j + 1}"); - result[i].Children![j].ZIndex.Should().Be(j + 1); - } - } - } - - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void Deserialize_List_Should_Ignore_ZIndex(bool isControlIdentifiers) - { - var deserializer = CreateDeserializer(isControlIdentifiers); - - using var yamlStream = File.OpenRead(GetTestFilePath(@"_TestData/ValidYaml{0}/ZIndexOrdering/with-existing-zindex.pa.yaml", isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - var result = deserializer.Deserialize(yamlReader); - result.ShouldNotBeNull(); - if (result.Children == null) - { - Assert.Fail("Children should not be null"); - return; - } - result.Children.Should().HaveCount(5); - for (var i = 0; i < result.Children.Count; i++) - { - result.Children[i].Name.Should().Be($"Label{result.Children.Count - i}"); - result.Children[i].ZIndex.Should().Be(result.Children.Count - i); - } - } - - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void Seralizes_List_Should_Be_Without_ZIndex(bool isControlIdentifiers) - { - var graph = new List - { - MakeLabelWithZIndex(1), - MakeLabelWithZIndex(3), - MakeLabelWithZIndex(2), - MakeLabelWithZIndex(5), - MakeLabelWithZIndex(4), - }; - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.Serialize(graph); - var expected = File.ReadAllText(GetTestFilePath(@"_TestData/ValidYaml{0}/ZIndexOrdering/With-list-of-controls.pa.yaml", isControlIdentifiers)); - sut.Should().BeEquivalentTo(expected); - } - - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void Seralizes_SortedList_Should_Be_Without_ZIndex(bool isControlIdentifiers) - { - var graph = new SortedList - { - {1, MakeLabelWithZIndex(1) }, - {3, MakeLabelWithZIndex(3) }, - {2, MakeLabelWithZIndex(2) }, - {5, MakeLabelWithZIndex(5) }, - {4, MakeLabelWithZIndex(4) }, - }; - - var serializer = CreateSerializer(isControlIdentifiers); - - var sut = serializer.Serialize(graph.Values); - var expected = File.ReadAllText(GetTestFilePath(@"_TestData/ValidYaml{0}/ZIndexOrdering/With-sortedlist-of-controls.pa.yaml", isControlIdentifiers)); - sut.Should().BeEquivalentTo(expected); - } - - [TestMethod] - [DataRow(false)] - [DataRow(true)] - public void Host_should_have_no_ZIndex(bool isControlIdentifiers) - { - var deserializer = CreateDeserializer(isControlIdentifiers); - - using var yamlStream = File.OpenRead(GetTestFilePath(@"_TestData/ValidYaml{0}/App.pa.yaml", isControlIdentifiers)); - using var yamlReader = new StreamReader(yamlStream); - - var result = deserializer.Deserialize(yamlReader); - result.ShouldNotBeNull(); - if (result.Children == null) - { - Assert.Fail("Children should not be null"); - return; - } - result.Children.Should().HaveCount(1); - var host = result.Children[0]; - host.Name.Should().Be("Host"); - host.Properties.Should().NotContainKey(PropertyNames.ZIndex); - } - - private Control MakeLabelWithZIndex(int i) - { - return ControlFactory.Create($"Label{i}", template: "text", - properties: - new() - { - { PropertyNames.ZIndex, i.ToString(CultureInfo.InvariantCulture) }, - }); - } -} diff --git a/src/Persistence/Attributes/FirstClassAttribute.cs b/src/Persistence/Attributes/FirstClassAttribute.cs deleted file mode 100644 index 9d5da751..00000000 --- a/src/Persistence/Attributes/FirstClassAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Attributes; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)] -public class FirstClassAttribute : Attribute -{ - public FirstClassAttribute(string templateName) - { - if (string.IsNullOrWhiteSpace(templateName)) - throw new ArgumentException($"'{nameof(templateName)}' cannot be null or whitespace.", nameof(templateName)); - - TemplateName = templateName; - } - - public string TemplateName { get; } -} diff --git a/src/Persistence/Collections/ControlPropertiesCollection.cs b/src/Persistence/Collections/ControlPropertiesCollection.cs deleted file mode 100644 index 5b8c07da..00000000 --- a/src/Persistence/Collections/ControlPropertiesCollection.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Collections; - -public class ControlPropertiesCollection : NamedItemsCollection -{ - private static readonly Func _keySelector = v => v.Name; - - public ControlPropertiesCollection() : base(_keySelector) - { - } - - public ControlPropertiesCollection(IEnumerable values) : base(values, _keySelector) - { - } - - internal ControlPropertiesCollection(ReadOnlySpan> values) : base(values, _keySelector) - { - } - - internal ControlPropertiesCollection(IEnumerable> values) : base(values, _keySelector) - { - } - - /// - /// Used for collection expressions initialization syntax - /// - /// - /// - internal static ControlPropertiesCollection Create(scoped ReadOnlySpan> values) - { - return new(values); - } - - public void Add(string key, string? value) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentNullException(nameof(key)); - - Add(key, new ControlProperty(key, value)); - } - - public void Add(Tuple keyValue) - { - if (string.IsNullOrWhiteSpace(keyValue.Item1)) - throw new ArgumentNullException(nameof(keyValue), "The key cannot be null or whitespace."); - - Add(keyValue.Item1, new ControlProperty(keyValue.Item1, keyValue.Item2)); - } - - public static implicit operator ControlPropertiesCollection(Dictionary collection) - { - return new ControlPropertiesCollection(collection); - } -} diff --git a/src/Persistence/Collections/CustomPropertiesCollection.cs b/src/Persistence/Collections/CustomPropertiesCollection.cs deleted file mode 100644 index f368bb34..00000000 --- a/src/Persistence/Collections/CustomPropertiesCollection.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Collections; - -public class CustomPropertiesCollection : NamedItemsCollection -{ - private static readonly Func _keySelector = v => v.Name; - - public CustomPropertiesCollection() : base(_keySelector) - { - } - - public CustomPropertiesCollection(IEnumerable values) : base(values, _keySelector) - { - } - - internal CustomPropertiesCollection(IEnumerable> values) : base(values, _keySelector) { } - - public static implicit operator CustomPropertiesCollection(Dictionary dictionary) - { - return new CustomPropertiesCollection(dictionary); - } -} diff --git a/src/Persistence/Collections/CustomPropertyParametersCollection.cs b/src/Persistence/Collections/CustomPropertyParametersCollection.cs deleted file mode 100644 index b44239b2..00000000 --- a/src/Persistence/Collections/CustomPropertyParametersCollection.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Collections; - -public class CustomPropertyParametersCollection : NamedItemsCollection -{ - private static readonly Func _keySelector = v => v.Name; - - public CustomPropertyParametersCollection() : base(_keySelector) - { - } - - public CustomPropertyParametersCollection(IEnumerable values) : base(values, _keySelector) - { - } - - public static implicit operator CustomPropertyParametersCollection(CustomPropertyParameter[] items) - { - return new CustomPropertyParametersCollection(items); - } -} diff --git a/src/Persistence/Collections/NamedItemsCollection.cs b/src/Persistence/Collections/NamedItemsCollection.cs deleted file mode 100644 index 587e2b19..00000000 --- a/src/Persistence/Collections/NamedItemsCollection.cs +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Collections; - -[DebuggerDisplay("Count = {Count}")] -public abstract class NamedItemsCollection : - IDictionary, - IDictionary where - TValue : notnull -{ - private readonly Dictionary _items = new(); - private readonly Func _keySelector; - - protected NamedItemsCollection(Func keySelector) - { - _keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector)); - } - - protected NamedItemsCollection(IEnumerable values, Func keySelector) : this(keySelector) - { - foreach (var item in values) - { - _items.Add(keySelector(item), item); - } - } - - protected NamedItemsCollection(IEnumerable> values, Func keySelector) : this(keySelector) - { - foreach (var kvp in values) - { - _items.Add(kvp.Key, kvp.Value); - } - } - - protected NamedItemsCollection(ReadOnlySpan> values, Func keySelector) : this(keySelector) - { - foreach (var kvp in values) - { - _items.Add(kvp.Key, kvp.Value); - } - } - - public TValue this[string key] - { - get => _items[key]; - set => _items[key] = value; - } - - public IEnumerable Keys => _items.Keys; - - public IEnumerable Values => _items.Values; - - public int Count => _items.Count; - - public bool IsReadOnly => false; - - public bool IsFixedSize => false; - - ICollection IDictionary.Keys => _items.Keys; - - ICollection IDictionary.Values => _items.Values; - - ICollection IDictionary.Keys => _items.Keys; - - ICollection IDictionary.Values => _items.Values; - - public bool IsSynchronized => true; - - public object SyncRoot => _items; - - public object? this[object key] - { - get - { - if (key is not string s) - throw new ArgumentException("Key must be a string", nameof(key)); - - return _items[s]; - } - set - { - if (key is not string s) - throw new ArgumentException("Key must be a string", nameof(key)); - if (key is not TValue v) - throw new ArgumentException($"Value must be of type {typeof(TValue).Name}", nameof(value)); - - _items[s] = v; - } - } - - public bool ContainsKey(string key) - { - return _items.ContainsKey(key); - } - - public void Add(TValue value) - { - if (value is null) - throw new ArgumentNullException(nameof(value)); - _items.Add(_keySelector(value), value); - } - - public void Add(string key, TValue value) - { - if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentNullException(nameof(key)); - - _items.Add(key, value); - } - - public void Add(KeyValuePair item) - { - _items.Add(item.Key, item.Value); - } - - public void Add(object key, object? value) - { - if (key is not string s) - throw new ArgumentException("Key must be a string", nameof(key)); - if (key is not TValue v) - throw new ArgumentException($"Value must be of type {typeof(TValue).Name}", nameof(value)); - - Add(s, v); - } - - public bool TryGetValue(string key, [MaybeNullWhen(false)] out TValue value) - { - return _items.TryGetValue(key, out value); - } - - public IEnumerator> GetEnumerator() - { - return _items.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _items.GetEnumerator(); - } - - IDictionaryEnumerator IDictionary.GetEnumerator() - { - return ((IDictionary)_items).GetEnumerator(); - } - - public bool Remove(string key) - { - return _items.Remove(key); - } - - public bool Remove(KeyValuePair item) - { - return _items.Remove(item.Key); - } - - public bool Remove(TValue value) - { - if (value is null) - throw new ArgumentNullException(nameof(value)); - return _items.Remove(_keySelector(value)); - } - - public void Clear() - { - _items.Clear(); - } - - public bool Contains(KeyValuePair item) - { - return _items.Contains(item); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - ((IDictionary)_items).CopyTo(array, arrayIndex); - } - - public bool Contains(object key) - { - if (key is string s) - return ContainsKey(s); - if (key is TValue v) - return ContainsKey(_keySelector(v)); - - throw new ArgumentException($"Key must be a string or of type {typeof(TValue).Name}", nameof(key)); - } - - public void Remove(object key) - { - Remove((string)key); - } - - public void CopyTo(Array array, int index) - { - throw new NotImplementedException(); - } -} diff --git a/src/Persistence/Extensions/DictionaryExtensions.cs b/src/Persistence/Extensions/DictionaryExtensions.cs deleted file mode 100644 index eb446c12..00000000 --- a/src/Persistence/Extensions/DictionaryExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; - -public static class DictionaryExtensions -{ - public static bool TryGetValue(this Dictionary dictionary, string name, [MaybeNullWhen(false)] out T? value) - { - value = default; - if (!dictionary.TryGetValue(name, out var valueObj)) - return false; - - if (valueObj == null) - return true; - - if (valueObj is T) - { - value = (T)valueObj; - return true; - } - - throw new InvalidCastException($"Value for key '{name}' is not of type '{typeof(T).Name}'."); - } -} diff --git a/src/Persistence/Extensions/ListExtensions.cs b/src/Persistence/Extensions/ListExtensions.cs deleted file mode 100644 index 015de348..00000000 --- a/src/Persistence/Extensions/ListExtensions.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; - -public static class ListExtensions -{ - /// - /// Sorts an IList in place. - /// - public static void Sort(this IList list, Comparison comparison) - { - ArrayList.Adapter((IList)list).Sort(new ComparisonComparer(comparison)); - } - - /// - /// Convenience method on IEnumerable to allow passing of a Comparison delegate to the OrderBy method. - /// - /// - /// - /// - /// - public static IEnumerable OrderBy(this IEnumerable list, Comparison comparison) - { - return list.OrderBy(t => t, new ComparisonComparer(comparison)); - } -} - -/// -/// Wraps a generic Comparison delegate in an IComparer -/// -/// -public class ComparisonComparer : IComparer, IComparer -{ - private readonly Comparison _comparison; - - public ComparisonComparer(Comparison comparison) - { - _comparison = comparison; - } - - public int Compare(T? x, T? y) - { - if (x == null && y == null) - return 0; - if (x == null) - return -1; - if (y == null) - return 1; - - return _comparison(x, y); - } - - public int Compare(object? x, object? y) - { - if (x == null && y == null) - return 0; - if (x == null) - return -1; - if (y == null) - return 1; - - return _comparison((T)x, (T)y); - } -} diff --git a/src/Persistence/Extensions/ServiceCollectionExtensions.cs b/src/Persistence/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index 0114e6e9..00000000 --- a/src/Persistence/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; - -public static class ServiceCollectionExtensions -{ - /// - /// registers the MSAPP persistence services - /// - /// the services collection instance. - /// if true, registers the default templates (eg. 'text', 'button') on the templates store. - public static void AddPowerAppsPersistence(this IServiceCollection services, bool useDefaultTemplates = false) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(ctx => - { - var store = new ControlTemplateStore(); - - AddMinimalTemplates(store); - - if (useDefaultTemplates) - AddDefaultTemplates(store); - - store.DiscoverBuiltInTemplateTypes(); - - return store; - }); - } - - private static void AddMinimalTemplates(ControlTemplateStore store) - { - store.Add(new() { Name = "hostControl", DisplayName = "host", Id = BuiltInTemplates.Host.Id }); - store.Add(new() { Name = "appinfo", DisplayName = "app", Id = BuiltInTemplates.App.Id }); - store.Add(new() { Name = "screen", Id = BuiltInTemplates.Screen.Id }); - store.Add(new() { Name = "component", Id = BuiltInTemplates.Component.Id }); - store.Add(new() { Name = "group", Id = BuiltInTemplates.Group.Id }); - - // Gallery - store.Add(new() - { - Name = "gallery", - Id = "http://microsoft.com/appmagic/gallery", - NestedTemplates = new ControlTemplate[] - { - new() - { - Name = "galleryTemplate", - Id = "http://microsoft.com/appmagic/galleryTemplate", - AddPropertiesToParent = true, - InputProperties = - { - { "ItemAccessibleLabel", string.Empty }, - { "TemplateFill", string.Empty }, - { "OnSelect", string.Empty } - } - } - } - }); - store.Add(new() { Name = "commandComponent", Id = "http://microsoft.com/appmagic/CommandComponent" }); - } - - /// - /// Adds some default templates which are useful for testing - /// - /// - private static void AddDefaultTemplates(ControlTemplateStore store) - { - store.Add(new() { Name = "text", Id = "http://microsoft.com/appmagic/text" }); - store.Add(new() { Name = "button", Id = "http://microsoft.com/appmagic/button" }); - store.Add(new() { Name = "label", Id = "http://microsoft.com/appmagic/label" }); - - store.Add(new() { Name = "TextCanvas", Id = "http://microsoft.com/appmagic/powercontrol/PowerApps_CoreControls_TextCanvas" }); - store.Add(new() { Name = "ButtonCanvas", Id = "http://microsoft.com/appmagic/powercontrol/PowerApps_CoreControls_ButtonCanvas" }); - - store.Add(new() { Name = "DataCard", Id = "http://microsoft.com/appmagic/card" }); - store.Add(new() { Name = "TypedDataCard", Id = "http://microsoft.com/appmagic/card" }); - - store.Add(new() { Name = "groupContainer", Id = "http://microsoft.com/appmagic/groupContainer" }); - } -} diff --git a/src/Persistence/Extensions/TypeExtensions.cs b/src/Persistence/Extensions/TypeExtensions.cs deleted file mode 100644 index 8dca589d..00000000 --- a/src/Persistence/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Attributes; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; - -internal static class TypeExtensions -{ - public static bool IsFirstClass(this Type type, out FirstClassAttribute? attribute) - { - var attributes = type.GetCustomAttributes(true) ?? Array.Empty(); - attribute = attributes.FirstOrDefault(a => a is FirstClassAttribute) as FirstClassAttribute; - return attribute is not null; - } -} diff --git a/src/Persistence/Extensions/YamlExtensions.cs b/src/Persistence/Extensions/YamlExtensions.cs deleted file mode 100644 index 0c57e604..00000000 --- a/src/Persistence/Extensions/YamlExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using YamlDotNet.Core; -using YamlDotNet.Core.Events; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; - -public static class YamlExtensions -{ - internal static readonly char[] LineTerminators = new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }; - - public static ScalarStyle DetermineScalarStyleForProperty(this string? property) - { - if (property == null) - { - return ScalarStyle.Plain; - } - else if (property.Any(c => LineTerminators.Contains(c))) - { - return ScalarStyle.Literal; - } - else if (property.Contains(" #") || property.Contains(": ")) - { - // These sequences break YAML parsing when outside of a literal block - return ScalarStyle.Literal; - } - else - { - return ScalarStyle.Plain; - } - } - - public static void Emit(this IEmitter emitter, string propertyName, string? propertyValue) - { - if (string.IsNullOrWhiteSpace(propertyValue)) - return; - - emitter.Emit(new Scalar(propertyName)); - emitter.Emit(new Scalar(null, null, propertyValue, propertyValue.DetermineScalarStyleForProperty(), true, false)); - } -} diff --git a/src/Persistence/Json/JsonDoubleToIntConverter.cs b/src/Persistence/Json/JsonDoubleToIntConverter.cs deleted file mode 100644 index 2578782a..00000000 --- a/src/Persistence/Json/JsonDoubleToIntConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Json; - -public class JsonDoubleToIntConverter : JsonConverter -{ - public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return reader.TryGetDouble(out var value) ? Convert.ToInt32(value) : reader.GetInt32(); - } - - public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, value, options); - } -} diff --git a/src/Persistence/Log.cs b/src/Persistence/Log.cs deleted file mode 100644 index 7d8dcd33..00000000 --- a/src/Persistence/Log.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Extensions.Logging; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence; - -internal static partial class Log -{ - [LoggerMessage(EventId = 1000, Level = LogLevel.Information, Message = "{message}")] - public static partial void InfoMessage(this ILogger logger, string message); - - [LoggerMessage(EventId = 2000, Level = LogLevel.Error, Message = "Duplicate entry found in archive: {entryFullName}")] - public static partial void DuplicateEntry(this ILogger logger, string entryFullName); -} diff --git a/src/Persistence/Microsoft.PowerPlatform.PowerApps.Persistence.csproj b/src/Persistence/Microsoft.PowerPlatform.PowerApps.Persistence.csproj index f4748665..5a38c8df 100644 --- a/src/Persistence/Microsoft.PowerPlatform.PowerApps.Persistence.csproj +++ b/src/Persistence/Microsoft.PowerPlatform.PowerApps.Persistence.csproj @@ -35,14 +35,9 @@ $(NoWarn);NU1601 - - - - - + - - \ No newline at end of file + diff --git a/src/Persistence/Models/App.cs b/src/Persistence/Models/App.cs deleted file mode 100644 index 0c43e3b1..00000000 --- a/src/Persistence/Models/App.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Attributes; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -/// -/// Represents an Canvas App. -/// -[FirstClass(templateName: "Appinfo")] -[YamlSerializable] -public record App : Control -{ - // App control has a special name. - public const string ControlName = "App"; - - public App() - { - } - - /// - /// Default constructor. - /// - [SetsRequiredMembers] - public App(string name, string variant, IControlTemplateStore controlTemplateStore) - { - Name = name; - Variant = variant; - Template = controlTemplateStore.GetByName(BuiltInTemplates.App.Name); - } - - [YamlIgnore] - public IList Screens { get; set; } = new List(); - - internal override void AfterCreate(Dictionary controlDefinition) - { - if (controlDefinition.TryGetValue>(nameof(Screens), out var screens)) - { - if (screens != null) - Screens = screens; - else - Screens = new List(); - } - } -} diff --git a/src/Persistence/Models/AppProperties.cs b/src/Persistence/Models/AppProperties.cs deleted file mode 100644 index 5de544c0..00000000 --- a/src/Persistence/Models/AppProperties.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -/// -/// App properties -/// -public record AppProperties -{ - [SetsRequiredMembers] - public AppProperties() - { - // Default values for AppPreviewFlags - PreviewFlags = new() { - { "datatablev2control", true }, - { "delayloadscreens", true }, - { "enableonstart", true }, - { "enablepcfmoderndatasets", true }, - { "enablesaveloadcleardataonweb", true }, - { "errorhandling", true }, - { "expandedsavedatasupport", true }, - { "fluentv9controlspreview", true }, - { "formuladataprefetch", true }, - { "reactformulabar", true }, - { "usenonblockingonstartrule", true }, - }; - } - - public required string Author { get; init; } = string.Empty; - public required string Name { get; init; } = string.Empty; - public required string Id { get; init; } = Guid.NewGuid().ToString(); - public string? FileID { get; init; } - public required string LocalConnectionReferences { get; init; } = string.Empty; - public required string LocalDatabaseReferences { get; init; } = string.Empty; - public string? LibraryDependencies { get; init; } - public string[] AppPreviewFlagsKey { get; init; } = Array.Empty(); - [JsonPropertyName("AppPreviewFlagsMap")] - public required Dictionary PreviewFlags { get; init; } - public double? DocumentLayoutWidth { get; init; } - public double? DocumentLayoutHeight { get; init; } - public string? DocumentLayoutOrientation { get; init; } - public bool? DocumentLayoutScaleToFit { get; init; } - public bool? DocumentLayoutMaintainAspectRatio { get; init; } - public bool? DocumentLayoutLockOrientation { get; init; } - public bool? ShowStatusBar { get; init; } - public string? OriginatingVersion { get; init; } - public string DocumentAppType { get; init; } = "DesktopOrTablet"; - public string DocumentType { get; init; } = "App"; - public string? AppCreationSource { get; init; } = "AppFromScratch"; - public string? AppDescription { get; init; } - public double? LastControlUniqueId { get; init; } - public double? DefaultConnectedDataSourceMaxGetRowsCount { get; init; } - public bool ContainsThirdPartyPcfControls { get; init; } - public double? ParserErrorCount { get; init; } - public double? BindingErrorCount { get; init; } - public string? InstrumentationKey { get; init; } - public bool EnableInstrumentation { get; init; } - public required Dictionary ControlCount { get; init; } = new(); - public double? DeserializationLoadTime { get; init; } - public double? AnalysisLoadTime { get; init; } - public string? ManualOfflineProfileId { get; init; } -} diff --git a/src/Persistence/Models/AppTemplates.cs b/src/Persistence/Models/AppTemplates.cs deleted file mode 100644 index fc4c6dee..00000000 --- a/src/Persistence/Models/AppTemplates.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -/// -/// App Templates -/// -internal sealed record AppTemplates -{ - [SetsRequiredMembers] - public AppTemplates() - { - } - - public required IList UsedTemplates { get; init; } = new List(); - public required IList? ComponentTemplates { get; init; } - public required IList? PcfTemplates { get; init; } -} diff --git a/src/Persistence/Models/AppThemes.cs b/src/Persistence/Models/AppThemes.cs deleted file mode 100644 index e3c2430e..00000000 --- a/src/Persistence/Models/AppThemes.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -internal sealed record AppThemes -{ - [SetsRequiredMembers] - public AppThemes() - { - } - - public required string CurrentTheme { get; init; } = "defaultTheme"; - public IList CustomThemes { get; init; } = new List(); -} diff --git a/src/Persistence/Models/BuiltInControl.cs b/src/Persistence/Models/BuiltInControl.cs deleted file mode 100644 index 813f530e..00000000 --- a/src/Persistence/Models/BuiltInControl.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public record BuiltInControl : Control -{ - public BuiltInControl() - { - } - - [SetsRequiredMembers] - public BuiltInControl(string name, string variant, ControlTemplate controlTemplate) : base(name, variant, controlTemplate) - { - } -} diff --git a/src/Persistence/Models/ComponentDefinition.cs b/src/Persistence/Models/ComponentDefinition.cs deleted file mode 100644 index f0e4f149..00000000 --- a/src/Persistence/Models/ComponentDefinition.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -/// -/// This class is meant to describe a Component definition. -/// -[YamlSerializable] -public record ComponentDefinition : Control -{ - protected ComponentDefinition() { } - - /// - /// Default constructor. - /// - [SetsRequiredMembers] - public ComponentDefinition(string name, string variant, ControlTemplate template) - : base(name, variant, template) - { - Type = template.Id switch - { - BuiltInTemplateIds.Component => ComponentType.Canvas, - BuiltInTemplateIds.CommandComponent => ComponentType.Command, - BuiltInTemplateIds.DataComponent => ComponentType.Data, - BuiltInTemplateIds.FunctionComponent => ComponentType.Function, - _ => throw new NotSupportedException($"Component type {template.Id} is not supported.") - }; - } - - [YamlMember(Order = 97)] - public string? Description { get; set; } - - /// - /// CanvasComponent, DataComponent, FunctionComponent, CommandComponent - /// - [YamlMember(Order = 98)] - public ComponentType Type { get; set; } = ComponentType.Canvas; - - [YamlMember(Order = 99)] - public bool AccessAppScope { get; set; } - - [YamlMember(Order = 100)] - public IList CustomProperties { get; set; } = new List(); - - internal override void AfterCreate(Dictionary controlDefinition) - { - if (controlDefinition.TryGetValue>(nameof(CustomProperties), out var customProperties)) - { - if (customProperties != null) - CustomProperties = customProperties; - } - - if (controlDefinition.TryGetValue(nameof(Description), out var description)) - { - Description = description; - } - - if (controlDefinition.TryGetValue(nameof(AccessAppScope), out var tmpString) && - bool.TryParse(tmpString, out var accessAppScope)) - { - AccessAppScope = accessAppScope; - } - } -} diff --git a/src/Persistence/Models/ComponentInstance.cs b/src/Persistence/Models/ComponentInstance.cs deleted file mode 100644 index 370b0a6a..00000000 --- a/src/Persistence/Models/ComponentInstance.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public record ComponentInstance : Control -{ - /// - /// The template id for a local component instance. - /// - public const string ComponentInstanceTemplateId = "http://localhost/Component"; - - public ComponentInstance() - { - } - - [SetsRequiredMembers] - public ComponentInstance(string name, string variant, ControlTemplate controlTemplate) : base(name, variant, controlTemplate) - { - } - - [YamlMember(Order = 1)] - public string ComponentName { get; set; } = string.Empty; - - [YamlMember(Order = 2)] - public string? ComponentLibraryUniqueName { get; set; } = string.Empty; -} diff --git a/src/Persistence/Models/ComponentType.cs b/src/Persistence/Models/ComponentType.cs deleted file mode 100644 index c4840144..00000000 --- a/src/Persistence/Models/ComponentType.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public enum ComponentType -{ - Canvas, - Data, - Function, - Command -} diff --git a/src/Persistence/Models/Control.cs b/src/Persistence/Models/Control.cs deleted file mode 100644 index faa9e20c..00000000 --- a/src/Persistence/Models/Control.cs +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -[DebuggerDisplay("{Template?.DisplayName}: {Name}")] -public abstract record Control : IConvertible -{ - public Control() - { - } - - [SetsRequiredMembers] - public Control(string name, string variant, ControlTemplate template) - { - Name = name; - Variant = variant; - Template = template; - } - - /// - /// template uri of the control. - /// - [YamlMember(Alias = YamlFields.Control, Order = 0)] - public string TemplateId => Template.Id; - - private string _name = string.Empty; - /// - /// the control's name. - /// - [YamlMember(Order = 10)] - public required string Name - { - get => _name; - set - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException(nameof(Name)); - - _name = value.Trim(); - } - } - - [YamlMember(Order = 20)] - public required string Variant { get; init; } = string.Empty; - - [YamlMember(Order = 30)] - public string Layout { get; set; } = string.Empty; - - /// - /// key/value pairs of Control properties. Mapped to/from Control rules. - /// - [YamlMember(Order = 40)] - public ControlPropertiesCollection Properties { get; set; } = new(); - - /// - /// list of child controls nested under this control. - /// This collection can be null in cases where the control does not support children. - /// - [YamlMember(Order = 50)] - public IList? Children { get; set; } - - [YamlIgnore] - public ControlEditorState? EditorState { get; set; } - - [YamlIgnore] - public required ControlTemplate Template { get; init; } - - [YamlIgnore] - public int ZIndex - { - get - { - if (Properties.TryGetValue(PropertyNames.ZIndex, out var prop) && int.TryParse(prop.Value, out var zIndex)) - return zIndex; - - return int.MaxValue; - } - } - - [YamlMember(Order = 60)] - public string StyleName { get; set; } = string.Empty; - - internal virtual void AfterCreate(Dictionary controlDefinition) - { - } - - /// - /// Called before serialization to hide nested templates which add properties to parent from YAML output. - /// - internal void HideNestedTemplates() - { - if (Children == null) - return; - - for (var i = 0; i < Children.Count; i++) - { - if (Children[i].Template.AddPropertiesToParent) - { - foreach (var childTemplateProperty in Children[i].Properties) - { - Properties.Add(childTemplateProperty.Key, childTemplateProperty.Value); - } - Children.RemoveAt(i); - i--; - } - } - } - - #region IConvertible - - public TypeCode GetTypeCode() - { - return TypeCode.Object; - } - - public bool ToBoolean(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(bool)}"); - } - - public byte ToByte(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(byte)}"); - } - - public char ToChar(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(char)}"); - } - - public DateTime ToDateTime(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(DateTime)}"); - } - - public decimal ToDecimal(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(decimal)}"); - } - - public double ToDouble(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(double)}"); - } - - public short ToInt16(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(short)}"); - } - - public int ToInt32(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(int)}"); - } - - public long ToInt64(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(long)}"); - } - - public sbyte ToSByte(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(sbyte)}"); - } - - public float ToSingle(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(float)}"); - } - - public string ToString(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(string)}"); - } - - public object ToType(Type conversionType, IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {conversionType.Name}"); - } - - public ushort ToUInt16(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(ushort)}"); - } - - public uint ToUInt32(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(uint)}"); - } - - public ulong ToUInt64(IFormatProvider? provider) - { - throw new NotSupportedException($"Cannot covert {GetType().Name} to {typeof(ulong)}"); - } - - #endregion -} diff --git a/src/Persistence/Models/ControlEditorState.cs b/src/Persistence/Models/ControlEditorState.cs deleted file mode 100644 index 6c74228d..00000000 --- a/src/Persistence/Models/ControlEditorState.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -/// -/// Per control, this is the Power Apps Studio state content that doesn't impact app functionality like IsLocked -/// -public record ControlEditorState -{ - /// - /// Constructor for serialization. - /// - public ControlEditorState() - { - } - - [SetsRequiredMembers] - public ControlEditorState(Control control) - { - Name = control.Name; - Template = control.Template; - } - - /// - /// Name. - /// - public required string Name { get; init; } - - public bool IsLocked { get; init; } - - /// - /// List of child control editor state nested under this control. - /// - [JsonPropertyName("Children")] - public IList? Controls { get; set; } - - /// - /// Temporary duplicated in the control editor state. - /// - public ControlTemplate? Template { get; init; } -} diff --git a/src/Persistence/Models/ControlProperty.cs b/src/Persistence/Models/ControlProperty.cs deleted file mode 100644 index 20fff089..00000000 --- a/src/Persistence/Models/ControlProperty.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -[DebuggerDisplay("{Value}")] -[SuppressMessage("Design", "CA1036:Override methods on comparable types", Justification = "REVIEW: Author of this class should remove this suppression and fix the violation, or update this Justification message.")] -public sealed record ControlProperty : IComparable -{ - public const char FormulaPrefix = '='; - - public ControlProperty() - { - } - - [SetsRequiredMembers] - public ControlProperty(string name, string? value) - { - Name = name; - Value = value; - } - - public static ControlProperty FromTextFirstString(string name, string? value) - { - if (value == null) - return new ControlProperty(name, $"\"\""); - - // If the value starts with the formula prefix, then it is a formula. - if (value.StartsWith(FormulaPrefix)) - return new ControlProperty(name, value[1..]) { IsFormula = true }; - - // Otherwise, it is a string value which should be in quotes. - return new ControlProperty(name, $"\"{value}\""); - } - - public int CompareTo(ControlProperty? other) - { - if (other == null) - return 1; - - if (Category != other.Category) - return Category.CompareTo(other.Category); - - return string.Compare(Name, other.Name, StringComparison.Ordinal); - } - - public static implicit operator ControlProperty(KeyValuePair property) - { - return new ControlProperty(property.Key, property.Value); - } - - public required string Name { get; init; } - - public string? Value { get; init; } - - public bool IsFormula { get; init; } - - public PropertyCategory Category { get; init; } = PropertyCategory.Unknown; -} diff --git a/src/Persistence/Models/CustomControl.cs b/src/Persistence/Models/CustomControl.cs deleted file mode 100644 index 73179006..00000000 --- a/src/Persistence/Models/CustomControl.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public record CustomControl : Control -{ - public CustomControl() - { - } - - [SetsRequiredMembers] - public CustomControl(string name, string variant, ControlTemplate controlTemplate) : base(name, variant, controlTemplate) - { - } -} diff --git a/src/Persistence/Models/CustomProperty.cs b/src/Persistence/Models/CustomProperty.cs deleted file mode 100644 index af1a319b..00000000 --- a/src/Persistence/Models/CustomProperty.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public record CustomProperty : INamedObject -{ - public required string Name { get; set; } - - public PropertyDirection Direction { get; init; } = PropertyDirection.Input; - - [YamlMember(Alias = "PropertyType")] - public PropertyType Type { get; init; } = PropertyType.Data; - - public string DataType { get; init; } = "String"; - - [YamlIgnore] - public PropertyCategory Category => Type switch - { - PropertyType.Action => PropertyCategory.Behavior, - PropertyType.Event => PropertyCategory.Behavior, - PropertyType.Data => PropertyCategory.Data, - PropertyType.Function => PropertyCategory.Data, - _ => throw new InvalidOperationException($"Invalid property type: {Type}") - }; - - public bool IsResettable { get; init; } - - public string? DisplayName { get; init; } - - public string? Description { get; init; } - - public string? Default { get; init; } - - public IList Parameters { get; set; } = new List(); - - public enum PropertyDirection - { - Input, - Output - } - - public enum PropertyType - { - Data, - Event, - Function, - Action - } -} diff --git a/src/Persistence/Models/CustomPropertyParameter.cs b/src/Persistence/Models/CustomPropertyParameter.cs deleted file mode 100644 index be01e80f..00000000 --- a/src/Persistence/Models/CustomPropertyParameter.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public record CustomPropertyParameter : INamedObject -{ - public required string Name { get; set; } = string.Empty; - public required string DataType { get; set; } = "String"; - public string? Description { get; set; } - public bool IsRequired { get; set; } -} diff --git a/src/Persistence/Models/DataSources.cs b/src/Persistence/Models/DataSources.cs deleted file mode 100644 index 386362df..00000000 --- a/src/Persistence/Models/DataSources.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public class DataSources -{ - [JsonPropertyName("DataSources")] - public List? Items { get; set; } -} - -public class DataSource -{ - public string Type { get; set; } = "ServiceInfo"; - public required string Name { get; set; } - public required string ServiceKind { get; set; } - public WadlMetadata? WadlMetadata { get; set; } - public required string ApiId { get; set; } -} - -public class WadlMetadata -{ - public required string WadlXml { get; set; } -} diff --git a/src/Persistence/Models/GroupControl.cs b/src/Persistence/Models/GroupControl.cs deleted file mode 100644 index 0b3ceae5..00000000 --- a/src/Persistence/Models/GroupControl.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Attributes; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -[FirstClass(templateName: "Group")] -[YamlSerializable] -public record GroupControl : Control -{ - public GroupControl() - { - } - - [SetsRequiredMembers] - public GroupControl(string name, string variant, IControlTemplateStore controlTemplateStore) - { - Name = name; - Variant = variant; - Template = controlTemplateStore.GetByName(BuiltInTemplates.Group.Name); - } -} diff --git a/src/Persistence/Models/Header.cs b/src/Persistence/Models/Header.cs deleted file mode 100644 index 996b3215..00000000 --- a/src/Persistence/Models/Header.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public record Header -{ - [SetsRequiredMembers] - public Header() - { - } - - public required Version DocVersion { get; init; } = new Version("1.337"); - public required Version MinVersionToLoad { get; init; } = new Version("1.337"); - public required Version MSAppStructureVersion { get; init; } = new Version("2.2.1"); - - public AnalysisOptionsHeader AnalysisOptions { get; init; } = new AnalysisOptionsHeader(); - - public record AnalysisOptionsHeader - { - [SetsRequiredMembers] - public AnalysisOptionsHeader() - { - } - - public required bool DataflowAnalysisEnabled { get; init; } = true; - public required bool DataflowAnalysisFlagStateToggledByUser { get; init; } - } -} diff --git a/src/Persistence/Models/PropertyCategory.cs b/src/Persistence/Models/PropertyCategory.cs deleted file mode 100644 index f0ae2862..00000000 --- a/src/Persistence/Models/PropertyCategory.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public enum PropertyCategory -{ - Behavior, - ConstantData, - Data, - Design, - Formulas, - Functions, - OnDemandData, - Scope, - Unknown, -} diff --git a/src/Persistence/Models/Resources.cs b/src/Persistence/Models/Resources.cs deleted file mode 100644 index 274e4fcd..00000000 --- a/src/Persistence/Models/Resources.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -public class Resources -{ - [JsonPropertyName("Resources")] - public IList Items { get; set; } = new List(); -} - -public class Resource -{ - public required string Name { get; init; } - public required string Schema { get; init; } - public bool IsSampleData { get; set; } - public bool IsWritable { get; set; } - public string Type { get; set; } = "ResourceInfo"; - public required string FileName { get; init; } - public required string Path { get; init; } - public required string Content { get; init; } - public string ResourceKind { get; set; } = "LocalFile"; - public string? RootPath { get; set; } -} diff --git a/src/Persistence/Models/Screen.cs b/src/Persistence/Models/Screen.cs deleted file mode 100644 index d252045b..00000000 --- a/src/Persistence/Models/Screen.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Attributes; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -[FirstClass(templateName: "Screen")] -[YamlSerializable] -public record Screen : Control -{ - /// - /// Default constructor. - /// - [SetsRequiredMembers] - public Screen(string name, string variant, IControlTemplateStore controlTemplateStore) - { - Name = name; - Variant = variant; - Template = controlTemplateStore.GetByName(BuiltInTemplates.Screen.Name); - } -} diff --git a/src/Persistence/Models/SourceLocation.cs b/src/Persistence/Models/SourceLocation.cs deleted file mode 100644 index 11caf43c..00000000 --- a/src/Persistence/Models/SourceLocation.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -/// -/// Source location -/// -[DebuggerDisplay("l:{Line}, c:{Column}, f:{FilePath}")] -public record SourceLocation -{ - /// - /// File path - /// - public string? FilePath { get; init; } - public int? Line { get; init; } - public int? Column { get; init; } - - /// - /// Default constructor - /// - public SourceLocation() - { - } - - /// - /// Parameterized constructor - /// - /// - /// - /// - public SourceLocation(string? filePath, int? line, int? column) - { - FilePath = filePath; - - if (line != null && line < 0) - throw new ArgumentOutOfRangeException(nameof(line)); - Line = line; - - if (column != null && column < 0) - throw new ArgumentOutOfRangeException(nameof(column)); - Column = column; - } - - /// - /// Copy constructor - /// - /// - public SourceLocation(SourceLocation sourceLocation) - { - FilePath = sourceLocation.FilePath; - Line = sourceLocation.Line; - Column = sourceLocation.Column; - } -} diff --git a/src/Persistence/MsApp/HeaderJson.cs b/src/Persistence/MsApp/HeaderJson.cs new file mode 100644 index 00000000..d8372d41 --- /dev/null +++ b/src/Persistence/MsApp/HeaderJson.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; + +/// +/// Model class for header.json file in msapp archive. +/// See same class in DocumentServer.Core for updated schema. +/// +internal sealed record HeaderJson +{ + public required Version DocVersion { get; init; } + public required Version MinVersionToLoad { get; init; } + public required Version MSAppStructureVersion { get; init; } + public DateTime? LastSavedDateTimeUTC { get; init; } + + public AnalysisOptionsHeader? AnalysisOptions { get; init; } + + public sealed record AnalysisOptionsHeader + { + public bool DataflowAnalysisEnabled { get; init; } + public bool DataflowAnalysisFlagStateToggledByUser { get; init; } + } +} diff --git a/src/Persistence/MsApp/IMsappArchive.cs b/src/Persistence/MsApp/IMsappArchive.cs index 0e05f563..38c7c5a3 100644 --- a/src/Persistence/MsApp/IMsappArchive.cs +++ b/src/Persistence/MsApp/IMsappArchive.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Compression; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; @@ -12,48 +11,37 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; /// public interface IMsappArchive : IDisposable { - /// - /// The app that is represented by the archive. - /// - App? App { get; set; } - - Version Version { get; } + Version MSAppStructureVersion { get; } Version DocVersion { get; } - AppProperties? Properties { get; set; } - - DataSources? DataSources { get; set; } - - Resources? Resources { get; set; } - - T Deserialize(string entryName, bool ensureRoundTrip = true) where T : Control; - /// - /// Saves control in the archive. Control can be App, Screen, or individual control. + /// Total sum of decompressed sizes of all entries in the archive. /// - void Save(Control control, string? directory = null); + long DecompressedSize { get; } /// - /// Saves the archive to the given stream or file. + /// Total sum of compressed sizes of all entries in the archive. /// - void Save(); + long CompressedSize { get; } /// - /// Total sum of decompressed sizes of all entries in the archive. + /// Provides access to the underlying zip archive. + /// Attention: This property might be removed in the future. /// - long DecompressedSize { get; } + ZipArchive ZipArchive { get; } /// - /// Total sum of compressed sizes of all entries in the archive. + /// Returns a new readonly dictionary instance containing of all entries in the archive. + /// The keys are normalized paths for the entry computed using . /// - long CompressedSize { get; } + IReadOnlyDictionary CanonicalEntries(); /// - /// Adds an image to the archive and registers it as a resource. + /// Adds a default .gitignore file to the root of the archive. /// - /// the name of the resource - string AddImage(string fileName, Stream imageStream); + /// Thrown if the archive is opened in Read mode or if a .gitignore entry already exists. + void AddGitIgnore(); /// /// Determine whether an entry with the given path exists in the archive. @@ -90,7 +78,7 @@ string GenerateUniqueEntryPath( /// /// /// the entry or null when not found. - ZipArchiveEntry? GetEntry(string entryName); + ZipArchiveEntry? GetEntryOrDefault(string entryName); /// /// Returns the entry in the archive with the given name or null when not found. @@ -109,16 +97,4 @@ string GenerateUniqueEntryPath( /// Returns all entries in the archive that are in the given directory. /// IEnumerable GetDirectoryEntries(string directoryName, string? extension = null, bool recursive = true); - - /// - /// Dictionary of all entries in the archive. - /// The keys are normalized paths for the entry computed using . - /// - IReadOnlyDictionary CanonicalEntries { get; } - - /// - /// Provides access to the underlying zip archive. - /// Attention: This property might be removed in the future. - /// - ZipArchive ZipArchive { get; } } diff --git a/src/Persistence/MsApp/MsAppServiceCollectionExtensions.cs b/src/Persistence/MsApp/MsAppServiceCollectionExtensions.cs new file mode 100644 index 00000000..db7094dc --- /dev/null +++ b/src/Persistence/MsApp/MsAppServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO.Compression; +using System.Text; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; + +public static class MsAppServiceCollectionExtensions +{ + /// + /// Registers the service with default implementation of . + /// + /// the services collection instance. + /// See parameter of same name on . + public static void AddMsappArchiveFactory(this IServiceCollection services, Encoding? entryNameEncoding = null) + { + services.AddSingleton(sp => + { + if (entryNameEncoding is null) + { + return MsappArchiveFactory.Default; + } + else + { + return new MsappArchiveFactory() { EntryNameEncoding = entryNameEncoding }; + } + }); + } +} diff --git a/src/Persistence/MsApp/MsappArchive.cs b/src/Persistence/MsApp/MsappArchive.cs index c8314dbd..24334252 100644 --- a/src/Persistence/MsApp/MsappArchive.cs +++ b/src/Persistence/MsApp/MsappArchive.cs @@ -7,10 +7,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; @@ -19,169 +16,34 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; /// public partial class MsappArchive : IMsappArchive, IDisposable { - #region Constants + private const string HeaderFileName = "Header.json"; - public static class Directories - { - public const string Src = "Src"; - public const string Controls = "Controls"; - public const string Components = "Components"; - public const string AppTests = "AppTests"; - public const string Assets = "Assets"; - public const string Images = "Images"; - public const string References = "References"; - public const string Resources = "Resources"; - } - - public const string MsappFileExtension = ".msapp"; - public const string YamlFileExtension = ".yaml"; - public const string YamlPaFileExtension = ".pa.yaml"; - public const string JsonFileExtension = ".json"; - public const string AppFileName = $"App{YamlPaFileExtension}"; - public const string HeaderFileName = "Header.json"; - public const string PropertiesFileName = "Properties.json"; - public const string TemplatesFileName = $"{Directories.References}/Templates.json"; - public const string ThemesFileName = $"{Directories.References}/Themes.json"; - public const string DataSourcesFileName = $"{Directories.References}/DataSources.json"; - public const string ResourcesFileName = $"{Directories.References}/Resources.json"; - - #endregion - - #region Fields - - private readonly Lazy> _canonicalEntries; - private App? _app; - private Header? _header; - private AppProperties? _appProperties; - private AppTemplates? _appTemplates; - private AppThemes? _appThemes; - private DataSources? _dataSources; - private Resources? _resources; - - private bool _isDisposed; - private readonly ILogger? _logger; - private readonly Stream _stream; - private readonly bool _leaveOpen; - - // Yaml serializer and deserializer - private readonly IYamlSerializer _yamlSerializer; - private readonly IYamlDeserializer _yamlDeserializer; - private static readonly JsonSerializerOptions JsonSerializerOptions = new() + private static readonly JsonSerializerOptions JsonDeserializeOptions = new() { PropertyNameCaseInsensitive = true, AllowTrailingCommas = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, - WriteIndented = true - }; - private static readonly JsonWriterOptions JsonWriterOptions = new() - { - Indented = true + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, // We don't want to fail if there are extra properties in the json }; - #endregion - - #region Internal classes - - /// - /// Helper class for deserializing the top level control editor state. - /// - private sealed class TopParentJson - { -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - public ControlEditorState? TopParent { get; set; } -#pragma warning restore CS0649 - } - - #endregion - - #region Constructors - - public MsappArchive(string path, IYamlSerializationFactory yamlSerializationFactory, ILogger? logger = null) - : this(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read), ZipArchiveMode.Read, leaveOpen: false, yamlSerializationFactory, logger) - { - } - - public MsappArchive(Stream stream, IYamlSerializationFactory yamlSerializationFactory, ILogger? logger = null) - : this(stream, ZipArchiveMode.Read, leaveOpen: false, entryNameEncoding: null, yamlSerializationFactory, logger) - { - } - - public MsappArchive(Stream stream, ZipArchiveMode mode, IYamlSerializationFactory yamlSerializationFactory, ILogger? logger = null) - : this(stream, mode, leaveOpen: false, entryNameEncoding: null, yamlSerializationFactory, logger) - { - } + private ZipArchive? _zipArchive; + private Dictionary? _canonicalEntries; + private HeaderJson? _header; /// /// Constructor /// /// /// - /// - /// true to leave the stream open after the System.IO.Compression.ZipArchive object is disposed; otherwise, false - /// - /// - /// - public MsappArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, IYamlSerializationFactory yamlSerializationFactory, ILogger? logger = null) - : this(stream, mode, leaveOpen, null, yamlSerializationFactory, logger) - { - } - - public MsappArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? entryNameEncoding, IYamlSerializationFactory yamlSerializationFactory, ILogger? logger = null) + /// true to leave the stream open after the System.IO.Compression.ZipArchive object is disposed; otherwise, false + /// + public MsappArchive(Stream stream, ZipArchiveMode mode, bool leaveOpen = false, Encoding? entryNameEncoding = null) { - _stream = stream; - _leaveOpen = leaveOpen; - _yamlSerializer = yamlSerializationFactory.CreateSerializer(); - _yamlDeserializer = yamlSerializationFactory.CreateDeserializer(); - _logger = logger; - ZipArchive = new ZipArchive(stream, mode, leaveOpen, entryNameEncoding); - CreateGitIgnore(); - _canonicalEntries = new Lazy> - (() => - { - var canonicalEntries = new Dictionary(); - // If we're creating a new archive, there are no entries to canonicalize. - if (mode == ZipArchiveMode.Create) - return canonicalEntries; - foreach (var entry in ZipArchive.Entries) - { - if (!canonicalEntries.TryAdd(CanonicalizePath(entry.FullName), entry)) - _logger?.DuplicateEntry(entry.FullName); - } - return canonicalEntries; - }); + _zipArchive = new ZipArchive(stream, mode, leaveOpen, entryNameEncoding); } - #endregion - - #region Factory Methods - - public static IMsappArchive Create(string path, IYamlSerializationFactory yamlSerializationFactory, ILogger? logger = null) - { - var fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); - - return new MsappArchive(fileStream, ZipArchiveMode.Create, yamlSerializationFactory, logger); - } - - public static IMsappArchive Open(string path, IServiceProvider serviceProvider) - { - if (string.IsNullOrWhiteSpace(path)) - throw new ArgumentNullException(nameof(path), "Path cannot be null or whitespace."); - _ = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - - var yamlSerializationFactory = serviceProvider.GetRequiredService(); - - return new MsappArchive(path, yamlSerializationFactory); - } - - #endregion - - #region Properties - - /// - public IReadOnlyDictionary CanonicalEntries => _canonicalEntries.Value.AsReadOnly(); - /// - public ZipArchive ZipArchive { get; private set; } + public ZipArchive ZipArchive => _zipArchive ?? throw new ObjectDisposedException(nameof(MsappArchive)); /// /// Total sum of decompressed sizes of all entries in the archive. @@ -193,166 +55,60 @@ public static IMsappArchive Open(string path, IServiceProvider serviceProvider) /// public long CompressedSize => ZipArchive.Entries.Sum(zipArchiveEntry => zipArchiveEntry.CompressedLength); - public App? App - { - get - { - _app ??= LoadApp(); - return _app; - } - set - { - _app = value; - _header = _app != null ? new Header() : null; - _appProperties = _app != null ? new AppProperties() : null; - _appTemplates = _app != null ? new AppTemplates() : null; - _appThemes = _app != null ? new AppThemes() : null; - } - } + private HeaderJson Header => _header ??= LoadHeader(); - public Version Version - { - get - { - _header ??= LoadHeader(); + public Version MSAppStructureVersion => Header.MSAppStructureVersion; - return _header.MSAppStructureVersion; - } - } + public Version DocVersion => Header.DocVersion; - public Version DocVersion + private Dictionary InnerCanonicalEntries { get { - _header ??= LoadHeader(); - - return _header.DocVersion; - } - } - public AppProperties? Properties - { - get - { - _appProperties ??= LoadProperties(); - - return _appProperties; - } - set => _appProperties = value; - } - - public DataSources? DataSources - { - get - { - _dataSources ??= LoadDataSources(); + if (_canonicalEntries is null) + { + EnsureNotDisposed(); - return _dataSources; - } + var canonicalEntries = new Dictionary(); - set => _dataSources = value; - } + // In Create mode, we don't have access to the Entries, so we create it as empty. + // This should be fine, as this property is only used when adding entries. + if (ZipArchive.Mode != ZipArchiveMode.Create) + { + foreach (var entry in ZipArchive.Entries) + { + var canonicalizedPath = CanonicalizePath(entry.FullName); + if (!canonicalEntries.TryAdd(canonicalizedPath, entry)) + { + throw new InvalidDataException($"Duplicate canonicalized entry found in zip archive. EntryFullName: '{entry.FullName}'; CanonicalizedPath: '{canonicalizedPath}';"); + } + } + } - public Resources? Resources - { - get - { - _resources ??= LoadResources(); + _canonicalEntries = canonicalEntries; + } - return _resources; + return _canonicalEntries; } - - set => _resources = value; - } - - public bool AddGitIgnore { get; init; } = true; - - #endregion - - #region Methods - - /// - public bool DoesEntryExist(string entryPath) - { - _ = entryPath ?? throw new ArgumentNullException(nameof(entryPath)); - - return CanonicalEntries.ContainsKey(CanonicalizePath(entryPath)); } /// - public string AddImage(string fileName, Stream imageStream) + public IReadOnlyDictionary CanonicalEntries() { - if (string.IsNullOrWhiteSpace(fileName)) - throw new ArgumentNullException(nameof(fileName)); - ArgumentNullException.ThrowIfNull(imageStream); - - var imagePath = Path.Combine(Directories.Assets, Directories.Images, Path.GetFileName(fileName)); - if (TryGetEntry(imagePath, out _)) - throw new InvalidOperationException($"Image {fileName} already exists in the archive."); - - var imageEntry = CreateEntry(imagePath); - using (var entryStream = imageEntry.Open()) - { - imageStream.CopyTo(entryStream); - } - - // Register the image as a resource - var resourceName = Path.GetFileNameWithoutExtension(fileName); - _resources ??= new Resources(); - _resources.Items.Add(new Resource - { - Name = resourceName, - Schema = "i", - FileName = fileName, - Path = imagePath, - Content = "Image", - }); - - return resourceName; + return InnerCanonicalEntries.AsReadOnly(); } - /// - /// Deserializes the entry with the given name into an object of type T. - /// - /// - /// - /// - /// - /// - /// - /// - public T Deserialize(string entryName, bool ensureRoundTrip = true) where T : Control + private void EnsureNotDisposed() { - if (string.IsNullOrWhiteSpace(entryName)) - throw new ArgumentNullException(nameof(entryName)); - - var entry = GetRequiredEntry(entryName); - var result = Deserialize(entry); - - if (ensureRoundTrip) - { -#if DEBUG - // Expected round trip serialization - using var stringWriter = new StringWriter(); - _yamlSerializer.SerializeControl(stringWriter, result); -#endif - // Ensure round trip serialization - using var roundTripWriter = new RoundTripWriter(entry); - _yamlSerializer.SerializeControl(roundTripWriter, result); - } - - return result; + ObjectDisposedException.ThrowIf(_zipArchive is null, this); } - public T Deserialize(ZipArchiveEntry archiveEntry) where T : Control + /// + public bool DoesEntryExist(string entryPath) { - _ = archiveEntry ?? throw new ArgumentNullException(nameof(archiveEntry)); - - if (!archiveEntry.FullName.EndsWith(YamlFileExtension, StringComparison.OrdinalIgnoreCase)) - throw new PersistenceLibraryException(PersistenceErrorCode.MsappArchiveError, $"Entry {archiveEntry} is not a yaml file.") { MsappEntryFullPath = archiveEntry.FullName }; + _ = entryPath ?? throw new ArgumentNullException(nameof(entryPath)); - using var textReader = new StreamReader(archiveEntry.Open()); - return _yamlDeserializer.Deserialize(textReader) - ?? throw new PersistenceLibraryException(PersistenceErrorCode.EditorStateJsonEmptyOrNull, "Deserialization of file resulted in null object.") { MsappEntryFullPath = archiveEntry.FullName }; + return InnerCanonicalEntries.ContainsKey(CanonicalizePath(entryPath)); } /// @@ -363,28 +119,28 @@ public IEnumerable GetDirectoryEntries(string directoryName, st { directoryName = CanonicalizePath(directoryName).TrimEnd('/'); - foreach (var entry in CanonicalEntries) + foreach (var kvp in InnerCanonicalEntries) { // Do not return directories which some zip implementations include as entries - if (entry.Key.EndsWith('/')) + if (kvp.Key.EndsWith('/')) continue; - if (directoryName != string.Empty && !entry.Key.StartsWith(directoryName + '/', StringComparison.InvariantCulture)) + if (directoryName != string.Empty && !kvp.Key.StartsWith(directoryName + '/', StringComparison.InvariantCulture)) continue; // If not recursive, skip subdirectories - if (!recursive && entry.Key.IndexOf('/', directoryName.Length == 0 ? 0 : directoryName.Length + 1) > 0) + if (!recursive && kvp.Key.IndexOf('/', directoryName.Length == 0 ? 0 : directoryName.Length + 1) > 0) continue; - if (extension != null && !entry.Key.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) + if (extension != null && !kvp.Key.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) continue; - yield return entry.Value; + yield return kvp.Value; } } /// - public ZipArchiveEntry? GetEntry(string entryName) + public ZipArchiveEntry? GetEntryOrDefault(string entryName) { return TryGetEntry(entryName, out var entry) ? entry : null; } @@ -392,7 +148,7 @@ public IEnumerable GetDirectoryEntries(string directoryName, st /// public bool TryGetEntry(string entryName, [MaybeNullWhen(false)] out ZipArchiveEntry zipArchiveEntry) { - _ = entryName ?? throw new ArgumentNullException(nameof(entryName)); + ArgumentNullException.ThrowIfNull(entryName); if (string.IsNullOrWhiteSpace(entryName)) { @@ -400,7 +156,7 @@ public bool TryGetEntry(string entryName, [MaybeNullWhen(false)] out ZipArchiveE return false; } - return CanonicalEntries.TryGetValue(CanonicalizePath(entryName), out zipArchiveEntry); + return InnerCanonicalEntries.TryGetValue(CanonicalizePath(entryName), out zipArchiveEntry); } /// @@ -414,43 +170,18 @@ public ZipArchiveEntry GetRequiredEntry(string entryName) /// public ZipArchiveEntry CreateEntry(string entryName) { - if (string.IsNullOrWhiteSpace(entryName)) - throw new ArgumentNullException(nameof(entryName)); + ArgumentException.ThrowIfNullOrWhiteSpace(entryName); var canonicalEntryName = CanonicalizePath(entryName); - if (_canonicalEntries.Value.ContainsKey(canonicalEntryName)) + if (InnerCanonicalEntries.ContainsKey(canonicalEntryName)) throw new InvalidOperationException($"Entry {entryName} already exists in the archive."); var entry = ZipArchive.CreateEntry(entryName); - _canonicalEntries.Value.Add(canonicalEntryName, entry); + InnerCanonicalEntries.Add(canonicalEntryName, entry); return entry; } - /// - public void Save(Control control, string? directory = null) - { - _ = control ?? throw new ArgumentNullException(nameof(control)); - - var controlDirectory = directory == null ? Directories.Src : Path.Combine(Directories.Src, directory); - var entry = CreateEntry(GetSafeEntryPath(controlDirectory, control.Name, YamlPaFileExtension)); - - using (var writer = new StreamWriter(entry.Open())) - { - _yamlSerializer.SerializeControl(writer, control); - } - - SaveEditorState(control); - } - - private string GetSafeEntryPath(string directory, string name, string extension) - { - if (!TryMakeSafeForEntryPathSegment(name, out var safeName, unsafeCharReplacementText: "")) - throw new ArgumentException("Control name is not valid.", nameof(name)); - - return GenerateUniqueEntryPath(directory.WhiteSpaceToNull(), safeName, extension); - } - /// public string GenerateUniqueEntryPath( string? directory, @@ -491,35 +222,11 @@ public string GenerateUniqueEntryPath( throw new InvalidOperationException("Failed to generate a unique name."); } - public void Save() - { - if (_app == null || _header == null) - throw new InvalidOperationException("App or header are not set."); - - SaveHeader(); - SaveProperties(); - SaveTemplates(); - SaveThemes(); - SaveDataSources(); - SaveResources(); - - var appEntry = CreateEntry(Path.Combine(Directories.Src, AppFileName)); - using (var appWriter = new StreamWriter(appEntry.Open())) - { - _yamlSerializer.SerializeControl(appWriter, _app); - } - - foreach (var screen in _app.Screens) - { - Save(screen); - } - } - /// /// Canonicalizes an entry path to a value used in the canonical entries dictionary (). /// It removes leading and trailing slashes, converts backslashes to forward slashes, and makes the path lowercase. /// - public static string CanonicalizePath(string path) + public static string CanonicalizePath(string? path) { if (string.IsNullOrWhiteSpace(path)) return string.Empty; @@ -603,197 +310,20 @@ public static bool IsSafeForEntryPathSegment(string name) [GeneratedRegex(@"[/\\]+")] private static partial Regex EntryPathDirectorySeparatorsRegex(); - #endregion - - #region Private Methods - - private App? LoadApp() - { - // For app entry name is always "App.pa.yaml" now - if (!TryGetEntry(Path.Combine(Directories.Src, AppFileName), out var appEntry)) - return null; - - var app = Deserialize(appEntry.FullName, ensureRoundTrip: false); - - app.Screens = LoadScreens(); - - return app; - } - - private List LoadScreens() - { - _logger?.InfoMessage("Loading top level screens from Yaml."); - - var screens = new Dictionary(); - foreach (var yamlEntry in GetDirectoryEntries(Directories.Src, YamlFileExtension, recursive: false)) - { - // Skip the app file - if (yamlEntry.FullName.EndsWith(AppFileName, StringComparison.OrdinalIgnoreCase)) - continue; - - var screen = Deserialize(yamlEntry.FullName, ensureRoundTrip: false); - screens.Add(screen.Name, screen); - } - - _logger?.InfoMessage("Loading top level controls editor state."); - var controlEditorStates = new Dictionary(); - foreach (var editorStateEntry in GetDirectoryEntries(Path.Combine(Directories.Controls), JsonFileExtension)) - { - var topParentJson = DeserializeMsappJsonFile(editorStateEntry); - controlEditorStates.Add(topParentJson!.TopParent!.Name, topParentJson.TopParent); - } - - // Merge the editor state into the controls - foreach (var control in screens.Values) - { - if (controlEditorStates.TryGetValue(control.Name, out var editorState)) - { - MergeControlEditorState(control, editorState); - controlEditorStates.Remove(control.Name); - } - } - - return screens.Values.ToList(); - } - - private static void MergeControlEditorState(Control control, ControlEditorState controlEditorState) - { - control.EditorState = controlEditorState; - if (control.Children == null) - return; - - foreach (var child in control.Children) - { - if (controlEditorState.Controls == null) - continue; - - // Find the editor state for the child by name - var childEditorState = controlEditorState.Controls.FirstOrDefault(c => c.Name == child.Name); - if (childEditorState == null) - continue; - - MergeControlEditorState(child, childEditorState); - } - controlEditorState.Controls = null; - } - - private void SaveHeader() - { - var entry = CreateEntry(HeaderFileName); - using var entryStream = entry.Open(); - using var writer = new Utf8JsonWriter(entryStream, JsonWriterOptions); - JsonSerializer.Serialize(writer, _header, JsonSerializerOptions); - } - - private Header LoadHeader() + private HeaderJson LoadHeader() { var entry = GetRequiredEntry(HeaderFileName); - var header = DeserializeMsappJsonFile
(entry); + var header = DeserializeMsappJsonFile(entry); return header; } - private AppProperties? LoadProperties() - { - if (!TryGetEntry(PropertiesFileName, out var entry)) - return null; - - var appProperties = DeserializeMsappJsonFile(entry); - return appProperties; - } - - private DataSources? LoadDataSources() - { - if (!TryGetEntry(DataSourcesFileName, out var entry)) - return null; - - var dataSources = DeserializeMsappJsonFile(entry); - return dataSources; - } - - private Resources? LoadResources() - { - if (!TryGetEntry(ResourcesFileName, out var entry)) - return null; - - var resources = DeserializeMsappJsonFile(entry); - return resources; - } - - private void SaveProperties() - { - var entry = CreateEntry(PropertiesFileName); - using var entryStream = entry.Open(); - using var writer = new Utf8JsonWriter(entryStream, JsonWriterOptions); - JsonSerializer.Serialize(writer, _appProperties, JsonSerializerOptions); - } - - private void SaveTemplates() - { - var entry = CreateEntry(TemplatesFileName); - using var entryStream = entry.Open(); - using var writer = new Utf8JsonWriter(entryStream, JsonWriterOptions); - JsonSerializer.Serialize(writer, _appTemplates, JsonSerializerOptions); - } - - private void SaveThemes() - { - var entry = CreateEntry(ThemesFileName); - using var entryStream = entry.Open(); - using var writer = new Utf8JsonWriter(entryStream, JsonWriterOptions); - JsonSerializer.Serialize(writer, _appThemes, JsonSerializerOptions); - } - - private void SaveDataSources() - { - if (_dataSources == null || _dataSources.Items == null || _dataSources.Items.Count == 0) - return; - - var entry = CreateEntry(DataSourcesFileName); - using var entryStream = entry.Open(); - using var writer = new Utf8JsonWriter(entryStream, JsonWriterOptions); - JsonSerializer.Serialize(writer, _dataSources, JsonSerializerOptions); - } - - private void SaveResources() - { - if (_resources == null || _resources.Items == null || _resources.Items.Count == 0) - return; - - var entry = CreateEntry(ResourcesFileName); - using var entryStream = entry.Open(); - using var writer = new Utf8JsonWriter(entryStream, JsonWriterOptions); - JsonSerializer.Serialize(writer, _resources, JsonSerializerOptions); - } - - private void SaveEditorState(Control control) - { - if (control.EditorState == null) - return; - var entry = CreateEntry(GetSafeEntryPath(Directories.Controls, control.Name, JsonFileExtension)); - var topParent = new TopParentJson - { - TopParent = MapEditorState(control) - }; - - using var entryStream = entry.Open(); - using var writer = new Utf8JsonWriter(entryStream, JsonWriterOptions); - JsonSerializer.Serialize(writer, topParent, JsonSerializerOptions); - } - - private static ControlEditorState MapEditorState(Control control) - { - var editorState = control.EditorState ?? new ControlEditorState(control); - if (control.Children == null || control.Children.Count == 0) - return editorState; - - editorState.Controls = control.Children.Select(MapEditorState).ToList(); - return editorState; - } - - private void CreateGitIgnore() + /// + public void AddGitIgnore() { - if (!AddGitIgnore || ZipArchive.Mode != ZipArchiveMode.Create) - return; + if (ZipArchive.Mode == ZipArchiveMode.Read) + throw new InvalidOperationException("Cannot add .gitignore entry when the archive is opened in Read mode."); + if (DoesEntryExist(".gitignore")) + throw new InvalidOperationException("Cannot add .gitignore entry when it already exists."); var entry = ZipArchive.CreateEntry(".gitignore"); using var entryStream = entry.Open(); @@ -810,7 +340,7 @@ private static T DeserializeMsappJsonFile(ZipArchiveEntry entry) { try { - return JsonSerializer.Deserialize(entry.Open()) + return JsonSerializer.Deserialize(entry.Open(), JsonDeserializeOptions) ?? throw new PersistenceLibraryException(PersistenceErrorCode.EditorStateJsonEmptyOrNull, "Deserialization of json file resulted in null object.") { MsappEntryFullPath = entry.FullName }; } catch (JsonException ex) @@ -825,24 +355,18 @@ private static T DeserializeMsappJsonFile(ZipArchiveEntry entry) } } - #endregion - #region IDisposable protected virtual void Dispose(bool disposing) { - if (!_isDisposed) + if (disposing && _zipArchive != null) { - if (disposing) - { - ZipArchive.Dispose(); - if (!_leaveOpen) - { - _stream.Dispose(); - } - } - - _isDisposed = true; + // ZipArchive.Dispose() finishes writing the zip file with it's current contents when opened in Create or Update mode. + // It also disposes the underlying stream unless leaveOpen was set to true. + _zipArchive.Dispose(); + _zipArchive = null; + _canonicalEntries = null; + _header = null; } } diff --git a/src/Persistence/MsApp/MsappArchiveFactory.cs b/src/Persistence/MsApp/MsappArchiveFactory.cs index 6f6bcc4c..c4c225ec 100644 --- a/src/Persistence/MsApp/MsappArchiveFactory.cs +++ b/src/Persistence/MsApp/MsappArchiveFactory.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using System.IO.Compression; -using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; +using System.Text; namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; @@ -11,12 +11,13 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; ///
public class MsappArchiveFactory : IMsappArchiveFactory { - private readonly IYamlSerializationFactory _yamlSerializationFactory; + /// + /// Instance of MsappArchiveFactory where is `null`. + /// Helps with using in tests where logging is not needed. + /// + public static readonly MsappArchiveFactory Default = new(); - public MsappArchiveFactory(IYamlSerializationFactory yamlSerializationFactory) - { - _yamlSerializationFactory = yamlSerializationFactory ?? throw new ArgumentNullException(nameof(yamlSerializationFactory)); - } + public Encoding? EntryNameEncoding { get; init; } public IMsappArchive Create(string path, bool overwrite = false) { @@ -25,14 +26,14 @@ public IMsappArchive Create(string path, bool overwrite = false) var fileStream = new FileStream(path, overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None); - return new MsappArchive(fileStream, ZipArchiveMode.Create, _yamlSerializationFactory); + return Create(fileStream, leaveOpen: false); } public IMsappArchive Create(Stream stream, bool leaveOpen = false) { _ = stream ?? throw new ArgumentNullException(nameof(stream)); - return new MsappArchive(stream, ZipArchiveMode.Create, leaveOpen, _yamlSerializationFactory); + return new MsappArchive(stream, ZipArchiveMode.Create, leaveOpen, EntryNameEncoding); } public IMsappArchive Open(string path) @@ -40,14 +41,16 @@ public IMsappArchive Open(string path) if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path)); - return new MsappArchive(path, _yamlSerializationFactory); + var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + + return Open(fileStream, leaveOpen: false); } public IMsappArchive Open(Stream stream, bool leaveOpen = false) { _ = stream ?? throw new ArgumentNullException(nameof(stream)); - return new MsappArchive(stream, ZipArchiveMode.Read, leaveOpen, _yamlSerializationFactory); + return new MsappArchive(stream, ZipArchiveMode.Read, leaveOpen, EntryNameEncoding); } public IMsappArchive Update(string path) @@ -56,13 +59,14 @@ public IMsappArchive Update(string path) throw new ArgumentNullException(nameof(path)); var fileStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); - return new MsappArchive(fileStream, ZipArchiveMode.Update, leaveOpen: false, _yamlSerializationFactory); + + return Update(fileStream, leaveOpen: false); } public IMsappArchive Update(Stream stream, bool leaveOpen = false) { _ = stream ?? throw new ArgumentNullException(nameof(stream)); - return new MsappArchive(stream, ZipArchiveMode.Update, leaveOpen, _yamlSerializationFactory); + return new MsappArchive(stream, ZipArchiveMode.Update, leaveOpen, EntryNameEncoding); } } diff --git a/src/Persistence/MsApp/RoundTripWriter.cs b/src/Persistence/MsApp/RoundTripWriter.cs deleted file mode 100644 index 7b082222..00000000 --- a/src/Persistence/MsApp/RoundTripWriter.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.IO.Compression; -using System.Text; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.MsApp; - -/// -/// Writer which compares the output with the input during serialization. -/// -public class RoundTripWriter : TextWriter -{ - private const char NewLineChar = '\n'; - private readonly TextReader _input; - private readonly string _entryFullPath; - private int _lineNumber = 1; - private int _columnNumber; - private bool _exThrown; - - public RoundTripWriter(ZipArchiveEntry entry) - : this(new StreamReader(entry.Open()), entry.FullName) - { - } - - public RoundTripWriter(TextReader input, string entryFullPath) - { - _input = input; - _entryFullPath = entryFullPath; - } - - public override Encoding Encoding => Encoding.Default; - - public override void Write(char value) - { - _columnNumber++; - if (value == '\r') - { - base.Write(value); - return; - } - - // Read each char while skipping the \r - int inputValue; - while ((inputValue = _input.Read()) == '\r') ; - - if (inputValue == -1 || inputValue != value) - { - _exThrown = true; - throw new PersistenceLibraryException(PersistenceErrorCode.RoundTripValidationFailed, $"Round trip serialization failed") - { - MsappEntryFullPath = _entryFullPath, - LineNumber = _lineNumber, - Column = _columnNumber - }; - } - - if (value == NewLineChar) - { - _lineNumber++; - _columnNumber = 0; - } - - base.Write(value); - } - - protected override void Dispose(bool disposing) - { - if (!_exThrown) // Don't throw a new exception in finally block if an exception was already thrown - { - // We need to make sure that we have read all the input. - var inputValue = _input.Read(); - if (inputValue != -1) - { - throw new PersistenceLibraryException(PersistenceErrorCode.RoundTripValidationFailed, $"Round trip serialization failed. Additional input not read when disposing.") - { - MsappEntryFullPath = _entryFullPath, - LineNumber = _lineNumber, - Column = _columnNumber - }; - } - } - _input.Dispose(); - base.Dispose(disposing); - } -} diff --git a/src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs b/src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs index 44b900a0..58aacb0c 100644 --- a/src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs +++ b/src/Persistence/PaYaml/Serialization/PFxExpressionYamlConverter.cs @@ -11,6 +11,8 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; // BUG 27469059: Internal classes not accessible to test project. InternalsVisibleTo attribute added to csproj doesn't get emitted because GenerateAssemblyInfo is false. public class PFxExpressionYamlConverter : IYamlTypeConverter { + private static readonly char[] LineTerminators = ['\r', '\n', '\x85', '\x2028', '\x2029']; + private readonly PFxExpressionYamlFormattingOptions _formattingOptions; public PFxExpressionYamlConverter(PFxExpressionYamlFormattingOptions formattingOptions) @@ -55,9 +57,10 @@ public void WriteYaml(IEmitter emitter, object? value, Type type) // These char sequences are special to YAML parsers, so must be escaped or forced to literal block var forceLiteralBlock = expression.InvariantScript.Contains(" #") || expression.InvariantScript.Contains(": "); + // Force multi-line scripts to be literal blocks - forceLiteralBlock |= expression.InvariantScript.Contains('\n') - || expression.InvariantScript.Contains('\r'); + forceLiteralBlock |= LineTerminators.Any(expression.InvariantScript.Contains); + // Force literal block for Unicode characters to preserve them without escaping forceLiteralBlock |= expression.InvariantScript.Any(char.IsSurrogate); if (!forceLiteralBlock && !_formattingOptions.ForceLiteralBlockIfContainsAny.IsDefaultOrEmpty) diff --git a/src/Persistence/Templates/BuiltInTemplateIds.cs b/src/Persistence/Templates/BuiltInTemplateIds.cs deleted file mode 100644 index 9777890c..00000000 --- a/src/Persistence/Templates/BuiltInTemplateIds.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -public static class BuiltInTemplateIds -{ - public const string App = "http://microsoft.com/appmagic/appinfo"; - public const string Host = "http://microsoft.com/appmagic/hostcontrol"; - public const string Screen = "http://microsoft.com/appmagic/screen"; - - public const string Component = "http://microsoft.com/appmagic/Component"; - public const string FunctionComponent = "http://microsoft.com/appmagic/FunctionComponent"; - public const string DataComponent = "http://microsoft.com/appmagic/DataComponent"; - public const string CommandComponent = "http://microsoft.com/appmagic/CommandComponent"; - - // Group is the legacy group container template - public const string Group = "http://microsoft.com/appmagic/group"; - - // Group Container is the newer layout container template - public const string GroupContainer = "http://microsoft.com/appmagic/groupContainer"; -} diff --git a/src/Persistence/Templates/BuiltInTemplateNames.cs b/src/Persistence/Templates/BuiltInTemplateNames.cs deleted file mode 100644 index 835a551c..00000000 --- a/src/Persistence/Templates/BuiltInTemplateNames.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -public static class BuiltInTemplateNames -{ - public const string App = "Appinfo"; - public const string Host = "HostControl"; - public const string Screen = "Screen"; - - public const string Component = "Component"; - public const string FunctionComponent = "FunctionComponent"; - public const string DataComponent = "DataComponent"; - public const string CommandComponent = "CommandComponent"; - - // Group is the legacy group container template - public const string Group = "Group"; - - // Group Container is the newer layout container template - public const string GroupContainer = "GroupContainer"; -} diff --git a/src/Persistence/Templates/BuiltInTemplates.cs b/src/Persistence/Templates/BuiltInTemplates.cs deleted file mode 100644 index f8b56eeb..00000000 --- a/src/Persistence/Templates/BuiltInTemplates.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -public static class BuiltInTemplates -{ - public static readonly (string Name, string Id) App = (BuiltInTemplateNames.App, BuiltInTemplateIds.App); - public static readonly (string Name, string Id) Host = (BuiltInTemplateNames.Host, BuiltInTemplateIds.Host); - public static readonly (string Name, string Id) Screen = (BuiltInTemplateNames.Screen, BuiltInTemplateIds.Screen); - - public static readonly (string Name, string Id) Component = (BuiltInTemplateNames.Component, BuiltInTemplateIds.Component); - public static readonly (string Name, string Id) FunctionComponent = (BuiltInTemplateNames.FunctionComponent, BuiltInTemplateIds.FunctionComponent); - public static readonly (string Name, string Id) DataComponent = (BuiltInTemplateNames.DataComponent, BuiltInTemplateIds.DataComponent); - public static readonly (string Name, string Id) CommandComponent = (BuiltInTemplateNames.CommandComponent, BuiltInTemplateIds.CommandComponent); - - // Group is the legacy group container template - public static readonly (string Name, string Id) Group = (BuiltInTemplateNames.Group, BuiltInTemplateIds.Group); - - // Group Container is the newer layout container template - public static readonly (string Name, string Id) GroupContainer = (BuiltInTemplateNames.GroupContainer, BuiltInTemplateIds.Group); -} diff --git a/src/Persistence/Templates/ControlFactory.cs b/src/Persistence/Templates/ControlFactory.cs deleted file mode 100644 index 1a3b6348..00000000 --- a/src/Persistence/Templates/ControlFactory.cs +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -public class ControlFactory : IControlFactory -{ - private readonly IControlTemplateStore _controlTemplateStore; - - public ControlFactory(IControlTemplateStore controlTemplateStore) - { - _controlTemplateStore = controlTemplateStore ?? throw new ArgumentNullException(nameof(controlTemplateStore)); - } - - public Control Create(string name, string template, - string? componentDefinitionName = null, - string? componentLibraryUniqueName = null, - string? variant = null, - ControlPropertiesCollection? properties = null, IList? children = null) - { - if (TryCreateComponent(name, template, componentDefinitionName, componentLibraryUniqueName, variant, properties, children, controlDefinition: null, out var control)) - return control; - - if (_controlTemplateStore.TryGetByIdOrName(template, out var controlTemplate)) - { - if (TryCreateFirstClassControl(name, controlTemplate.Name, variant ?? string.Empty, properties, children, controlDefinition: null, out control)) - return control; - - return new BuiltInControl(name, variant ?? string.Empty, controlTemplate) - { - Properties = properties ?? new(), - Children = children - }; - } - - return new CustomControl(name, variant ?? string.Empty, new ControlTemplate(template)) - { - Properties = properties ?? new(), - Children = children - }; - } - - public Control Create(string name, string template, - string componentDefinitionName, - string componentLibraryUniqueName, - Dictionary? controlDefinition) - { - string? variant = null; - string? layout = null; - string? styleName = null; - ControlPropertiesCollection? properties = null; - IList? children = null; - - if (controlDefinition != null) - { - controlDefinition.TryGetValue(nameof(Control.Variant), out variant); - controlDefinition.TryGetValue(nameof(Control.Properties), out properties); - controlDefinition.TryGetValue(nameof(Control.Children), out children); - controlDefinition.TryGetValue(nameof(Control.Layout), out layout); - controlDefinition.TryGetValue(nameof(Control.StyleName), out styleName); - } - - if (TryCreateComponent(name, template, componentDefinitionName, componentLibraryUniqueName, variant, properties, children, controlDefinition, out var control)) - return control; - - if (_controlTemplateStore.TryGetByIdOrName(template, out var controlTemplate)) - { - if (TryCreateFirstClassControl(name, controlTemplate.Name, variant ?? string.Empty, properties, children, controlDefinition, out control)) - { - control.StyleName = styleName ?? string.Empty; - return control; - } - - return new BuiltInControl(name, variant ?? string.Empty, controlTemplate) - { - Properties = properties ?? new(), - Children = children, - Layout = layout ?? string.Empty, - StyleName = styleName ?? string.Empty, - }; - } - - return new CustomControl(name, variant ?? string.Empty, new ControlTemplate(template)) - { - Properties = properties ?? new(), - Children = children, - Layout = layout ?? string.Empty, - StyleName = styleName ?? string.Empty, - }; - } - - public Control Create(string name, ControlTemplate template, - string? componentDefinitionName = null, - string? componentLibraryUniqueName = null, - string? variant = null, - ControlPropertiesCollection? properties = null, IList? children = null) - { - if (TryCreateComponent(name, template, componentDefinitionName, componentLibraryUniqueName, variant, properties, children, controlDefinition: null, out var control)) - return control; - - if (TryCreateFirstClassControl(name, template.Name, variant ?? string.Empty, properties, children, controlDefinition: null, out control)) - return control; - - return new BuiltInControl(name, variant ?? string.Empty, template) - { - Properties = properties ?? new(), - Children = children - }; - } - - public App CreateApp(ControlPropertiesCollection? properties = null) - { - return new App(App.ControlName, string.Empty, _controlTemplateStore) - { - Properties = properties ?? new(), - Children = new Control[] { Create("Host", BuiltInTemplates.Host.Name) } - }; - } - - public Screen CreateScreen(string name, string? variant = null, ControlPropertiesCollection? properties = null, IList? children = null) - { - return new Screen(name, variant ?? string.Empty, _controlTemplateStore) - { - Properties = properties ?? new(), - Children = children - }; - } - - private bool TryCreateComponent(string name, string template, - string? componentDefinitionName, - string? componentLibraryUniqueName, - string? variant, - ControlPropertiesCollection? properties, IList? children, Dictionary? controlDefinition, - [MaybeNullWhen(false)] out Control control) - { - control = null; - - if (!string.IsNullOrWhiteSpace(componentDefinitionName)) - { - control = new ComponentInstance(name, variant ?? string.Empty, - new ControlTemplate(ComponentInstance.ComponentInstanceTemplateId) { Name = componentDefinitionName }) - { - ComponentName = componentDefinitionName, - ComponentLibraryUniqueName = componentLibraryUniqueName, - Properties = properties ?? new(), - Children = children - }; - } - else if (template.Equals(BuiltInTemplates.Component.Name, StringComparison.OrdinalIgnoreCase) || - template.Equals(BuiltInTemplates.CommandComponent.Name, StringComparison.OrdinalIgnoreCase) || - template.Equals(BuiltInTemplates.DataComponent.Name, StringComparison.OrdinalIgnoreCase) || - template.Equals(BuiltInTemplates.FunctionComponent.Name, StringComparison.OrdinalIgnoreCase)) - { - controlDefinition ??= new(); - if (!controlDefinition.ContainsKey(nameof(ComponentDefinition.Type))) - controlDefinition[nameof(ComponentDefinition.Type)] = template switch - { - BuiltInTemplateNames.Component => nameof(ComponentType.Canvas), - BuiltInTemplateNames.CommandComponent => nameof(ComponentType.Command), - BuiltInTemplateNames.DataComponent => nameof(ComponentType.Data), - BuiltInTemplateNames.FunctionComponent => nameof(ComponentType.Function), - _ => "" - }; - - control = new ComponentDefinition(name, variant ?? string.Empty, CreateComponentTemplate(name, controlDefinition)) - { - Properties = properties ?? new(), - Children = children - }; - } - - if (control != null && controlDefinition != null) - control.AfterCreate(controlDefinition); - - return control != null; - } - - private static bool TryCreateComponent(string name, ControlTemplate template, - string? componentDefinitionName, - string? componentLibraryUniqueName, - string? variant, - ControlPropertiesCollection? properties, IList? children, Dictionary? controlDefinition, - [MaybeNullWhen(false)] out Control control) - { - control = null; - -#pragma warning disable CA1031 // Do not catch general exception types - try - { - if (!string.IsNullOrWhiteSpace(componentDefinitionName)) - { - control = new ComponentInstance(name, variant ?? string.Empty, template) - { - ComponentName = componentDefinitionName, - ComponentLibraryUniqueName = componentLibraryUniqueName, - Properties = properties ?? new(), - Children = children - }; - } - else - { - control = new ComponentDefinition(name, variant ?? string.Empty, template) - { - Properties = properties ?? new(), - Children = children - }; - } - } - catch - { - return false; - } -#pragma warning restore CA1031 // Do not catch general exception types - - if (controlDefinition != null) - control.AfterCreate(controlDefinition); - - return true; - } - - public ControlTemplate CreateComponentTemplate(string name, Dictionary? controlDefinition) - { - if (controlDefinition == null || !controlDefinition.TryGetValue(nameof(ComponentDefinition.Type), out var componentType)) - return new ControlTemplate(BuiltInTemplates.Component.Id) { Name = name }; - - if (!Enum.TryParse(componentType, out var componentTypeEnum)) - return new ControlTemplate(BuiltInTemplates.Component.Id) { Name = name }; - - return CreateComponentTemplate(name, componentTypeEnum); - } - - public ControlTemplate CreateComponentTemplate(string name, ComponentType componentType) - { - switch (componentType) - { - case ComponentType.Canvas: - return new ControlTemplate(BuiltInTemplates.Component.Id) { Name = name }; - case ComponentType.Data: - return new ControlTemplate(BuiltInTemplates.DataComponent.Id) { Name = name }; - case ComponentType.Function: - return new ControlTemplate(BuiltInTemplates.FunctionComponent.Id) { Name = name }; - case ComponentType.Command: - return new ControlTemplate(BuiltInTemplates.CommandComponent.Id) { Name = name }; - default: - return new ControlTemplate(BuiltInTemplates.Component.Id) { Name = name }; - } - } - - private bool TryCreateFirstClassControl(string name, string template, string variant, - ControlPropertiesCollection? properties, IList? children, Dictionary? controlDefinition, - [MaybeNullWhen(false)] out Control control) - { - control = null; - if (!_controlTemplateStore.TryGetControlTypeByName(template, out var controlType)) - return false; - - var instance = Activator.CreateInstance(controlType, name, variant, _controlTemplateStore); - if (instance is not Control controlInstance) - throw new InvalidOperationException($"Failed to create control of type {controlType.Name}."); - - if (properties is not null) - controlInstance.Properties = properties; - - controlInstance.Children = children; - if (controlDefinition != null) - controlInstance.AfterCreate(controlDefinition); - - control = controlInstance; - - return true; - } -} diff --git a/src/Persistence/Templates/ControlTemplate.cs b/src/Persistence/Templates/ControlTemplate.cs deleted file mode 100644 index 7117543a..00000000 --- a/src/Persistence/Templates/ControlTemplate.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -[DebuggerDisplay("{Name}")] -public record ControlTemplate -{ - public ControlTemplate() - { - } - - /// - /// Custom templates might have only id. - /// - /// - [SetsRequiredMembers] - public ControlTemplate(string id) - { - Id = id; - _name = id; - } - - public required string Id { get; init; } - - private string _name = string.Empty; - - public string Name - { - get => _name; - init - { - if (string.IsNullOrWhiteSpace(value)) - throw new ArgumentException(nameof(Name)); - - _name = value.Trim().FirstCharToUpper(); - } - } - - private string? _displayName; - - public string DisplayName - { - get => string.IsNullOrWhiteSpace(_displayName) ? _name : _displayName; - init - { - if (string.IsNullOrWhiteSpace(value)) - _displayName = null; - else - _displayName = value.Trim().FirstCharToUpper(); - } - } - - public bool HasDisplayName => !string.IsNullOrWhiteSpace(_displayName); - - public bool AddPropertiesToParent { get; init; } - - public ControlPropertiesCollection InputProperties { get; init; } = new(); - - public IList? NestedTemplates { get; init; } - - public CustomComponentInfo? ComponentInfo { get; init; } - - [MemberNotNullWhen(true, nameof(ComponentInfo))] - public bool IsCustomComponent => ComponentInfo != null; - - public bool IsPcfControlTemplate { get; init; } - - public class CustomComponentInfo - { - /// - /// The unique id (a form of guid) of the component as serialized in the app. - /// - public required string UniqueId { get; init; } - - /// - /// The friendly identifier of the component. - /// - public required string Name { get; init; } - } -} diff --git a/src/Persistence/Templates/ControlTemplateStore.cs b/src/Persistence/Templates/ControlTemplateStore.cs deleted file mode 100644 index ddacc28a..00000000 --- a/src/Persistence/Templates/ControlTemplateStore.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Attributes; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -/// -/// Control template store. -/// -public class ControlTemplateStore : IControlTemplateStore -{ - private readonly Dictionary _controlTemplatesByName = new(); - private readonly Dictionary> _controlTemplatesById = new(); - private readonly Dictionary _nameToType = new(); - private readonly Dictionary _typeToName = new(); - private readonly Dictionary _typeToTemplate = new(); - - public ControlTemplateStore() - { - } - - public void DiscoverBuiltInTemplateTypes() - { - var types = typeof(Control).Assembly.DefinedTypes; - foreach (var type in types) - { - // Ignore anything that isn't a control. - if (!type.IsAssignableTo(typeof(Control))) - continue; - - if (type.GetCustomAttributes(true).FirstOrDefault(a => a is FirstClassAttribute) is FirstClassAttribute firstClassAttribute) - { - var controlTemplate = _controlTemplatesByName[firstClassAttribute.TemplateName]; - - _nameToType.Add(controlTemplate.Name, type); - if (controlTemplate.HasDisplayName) - _nameToType.Add(controlTemplate.DisplayName, type); - - _typeToName.Add(type, controlTemplate.Name); - _typeToTemplate.Add(type, controlTemplate); - } - } - } - - public void Add(ControlTemplate controlTemplate) - { - _ = controlTemplate ?? throw new ArgumentNullException(nameof(controlTemplate)); - - var templateName = controlTemplate.Name.FirstCharToUpper(); - if (_controlTemplatesByName.ContainsKey(templateName)) - return; - - _controlTemplatesByName.Add(templateName, controlTemplate); - if (controlTemplate.HasDisplayName) - { - if (_controlTemplatesByName.ContainsKey(controlTemplate.DisplayName)) - return; - - _controlTemplatesByName.Add(controlTemplate.DisplayName, controlTemplate); - } - - // There can be multiple control templates with the same id, so we store them in a list. - if (!_controlTemplatesById.TryGetValue(controlTemplate.Id, out var controlTemplates)) - { - controlTemplates = new List(); - _controlTemplatesById.Add(controlTemplate.Id, controlTemplates); - } - controlTemplates.Add(controlTemplate); - - // Add nested templates. - if (controlTemplate.NestedTemplates != null) - { - foreach (var nestedTemplate in controlTemplate.NestedTemplates) - { - Add(nestedTemplate); - } - } - } - - /// - /// Returns the control template with the given name. - /// - public bool TryGetTemplateByName(string name, [MaybeNullWhen(false)] out ControlTemplate controlTemplate) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullException(nameof(name)); - - return _controlTemplatesByName.TryGetValue(name.FirstCharToUpper(), out controlTemplate); - } - - public bool TryGetControlTypeByName(string name, [MaybeNullWhen(false)] out Type controlType) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullException(nameof(name)); - - return _nameToType.TryGetValue(name, out controlType); - } - - /// - /// Returns the control template with the given name. - /// - public ControlTemplate GetByName(string name) - { - return _controlTemplatesByName[name]; - } - - /// - /// Returns the control template with the given id. - /// - public bool TryGetById(string id, [MaybeNullWhen(false)] out ControlTemplate controlTemplate) - { - if (!_controlTemplatesById.TryGetValue(id, out var controlTemplates)) - { - controlTemplate = null; - return false; - } - - // If there are multiple control templates with the same id, we return the first one. - controlTemplate = controlTemplates.First(); - return true; - } - - /// - /// Returns the control template with the given id or name. - /// - public bool TryGetByIdOrName(string id, [MaybeNullWhen(false)] out ControlTemplate controlTemplate) - { - if (TryGetTemplateByName(id, out controlTemplate)) - return true; - - if (TryGetById(id, out controlTemplate)) - return true; - - return false; - } - - public bool TryGetByType(Type type, [MaybeNullWhen(false)] out ControlTemplate controlTemplate) - { - return _typeToTemplate.TryGetValue(type, out controlTemplate); - } - - /// - /// Returns the control template with the given id. - /// - public ControlTemplate GetById(string id) - { - if (!TryGetById(id, out var controlTemplate)) - throw new KeyNotFoundException($"Control template with id '{id}' not found."); - - return controlTemplate; - } - - public bool Contains(Type type) - { - if (type == typeof(BuiltInControl)) - return true; - - return _typeToName.ContainsKey(type); - } - - public bool Contains(string name) - { - return _controlTemplatesByName.ContainsKey(name); - } - - public bool TryGetName(Type type, [MaybeNullWhen(false)] out string name) - { - return _typeToName.TryGetValue(type, out name); - } - - public Type GetControlType(string name) - { - // First check if we have concrete type for the name. - if (_nameToType.TryGetValue(name, out var type)) - return type; - - // If not, check if we have a control template for the name. - if (_controlTemplatesByName.TryGetValue(name, out _)) - return typeof(BuiltInControl); - - // If not, return the custom control type. - return typeof(CustomControl); - } -} diff --git a/src/Persistence/Templates/IControlFactory.cs b/src/Persistence/Templates/IControlFactory.cs deleted file mode 100644 index 70d72fc5..00000000 --- a/src/Persistence/Templates/IControlFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "REVIEW: Consider fixing by renaming parameters or update this Justification.")] -public interface IControlFactory -{ - Control Create(string name, string template, string? componentDefinitionName = null, string? componentLibraryUniqueName = null, string? variant = null, ControlPropertiesCollection? properties = null, IList? children = null); - - Control Create(string name, ControlTemplate template, string? componentDefinitionName = null, string? componentLibraryUniqueName = null, string? variant = null, ControlPropertiesCollection? properties = null, IList? children = null); - - Control Create(string name, string template, string componentDefinitionName, string componentLibraryUniqueName, Dictionary? controlDefinition); - - App CreateApp(ControlPropertiesCollection? properties = null); - - Screen CreateScreen(string name, string? variant = null, ControlPropertiesCollection? properties = null, IList? children = null); - - ControlTemplate CreateComponentTemplate(string name, Dictionary? controlDefinition); - - ControlTemplate CreateComponentTemplate(string name, ComponentType componentType); -} diff --git a/src/Persistence/Templates/IControlTemplateStore.cs b/src/Persistence/Templates/IControlTemplateStore.cs deleted file mode 100644 index 4152ebd1..00000000 --- a/src/Persistence/Templates/IControlTemplateStore.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -public interface IControlTemplateStore -{ - void Add(ControlTemplate controlTemplate); - - void DiscoverBuiltInTemplateTypes(); - - bool TryGetTemplateByName(string name, [MaybeNullWhen(false)] out ControlTemplate controlTemplate); - - bool TryGetControlTypeByName(string name, [MaybeNullWhen(false)] out Type controlType); - - ControlTemplate GetByName(string name); - - bool TryGetById(string id, [MaybeNullWhen(false)] out ControlTemplate controlTemplate); - - bool TryGetByIdOrName(string id, [MaybeNullWhen(false)] out ControlTemplate controlTemplate); - - bool TryGetByType(Type type, [MaybeNullWhen(false)] out ControlTemplate controlTemplate); - - ControlTemplate GetById(string id); - - bool Contains(Type type); - - bool Contains(string name); - - bool TryGetName(Type type, [MaybeNullWhen(false)] out string name); - - Type GetControlType(string name); -} diff --git a/src/Persistence/Yaml/AppConverter.cs b/src/Persistence/Yaml/AppConverter.cs deleted file mode 100644 index aa634266..00000000 --- a/src/Persistence/Yaml/AppConverter.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Core; -using YamlDotNet.Serialization.Utilities; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal sealed class AppConverter : ControlConverter -{ - public AppConverter(IControlFactory controlFactory) : base(controlFactory) - { - } - - public override bool Accepts(Type type) - { - return type == typeof(App); - } - - public override void OnWriteAfterName(IEmitter emitter, Control value) - { - if (value == null) - return; - - var app = (App)value; - - if (app.Screens != null) - { - emitter.Emit(new YamlDotNet.Core.Events.Scalar(nameof(App.Screens))); - ValueSerializer!.SerializeValue(emitter, app.Screens, typeof(List)); - } - - base.OnWriteAfterName(emitter, value); - } - - public override object? ReadKey(IParser parser, string key) - { - if (key == nameof(App.Screens)) - { - using var serializerState = new SerializerState(); - return ValueDeserializer!.DeserializeValue(parser, typeof(List), serializerState, ValueDeserializer); - } - - return base.ReadKey(parser, key); - } -} diff --git a/src/Persistence/Yaml/ComponentConverter.cs b/src/Persistence/Yaml/ComponentConverter.cs deleted file mode 100644 index bafbf64d..00000000 --- a/src/Persistence/Yaml/ComponentConverter.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization.Utilities; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal sealed class ComponentConverter : ControlConverter -{ - public ComponentConverter(IControlFactory controlFactory) : base(controlFactory) - { - } - - public override bool Accepts(Type type) - { - return type == typeof(ComponentDefinition) || type == typeof(ComponentInstance); - } - - public override object? ReadKey(IParser parser, string key) - { - if (key == nameof(ComponentDefinition.CustomProperties)) - { - using var serializerState = new SerializerState(); - return ValueDeserializer!.DeserializeValue(parser, typeof(List), serializerState, ValueDeserializer); - } - - return base.ReadKey(parser, key); - } - - public override string GetControlTemplateName(Control control) - { - return BuiltInTemplates.Component.Name; - } - - public override void OnWriteAfterName(IEmitter emitter, Control control) - { - // Nothing special to write for ComponentInstance - if (control is ComponentInstance componentInstance) - { - emitter.Emit(nameof(ComponentInstance.ComponentName), componentInstance.ComponentName); - emitter.Emit(nameof(ComponentInstance.ComponentLibraryUniqueName), componentInstance.ComponentLibraryUniqueName); - base.OnWriteAfterName(emitter, control); - return; - } - - var component = (ComponentDefinition)control; - - emitter.Emit(nameof(ComponentDefinition.Description), component.Description); - base.OnWriteAfterName(emitter, control); - - if (component.Type != ComponentType.Canvas) - { - emitter.Emit(nameof(ComponentDefinition.Type), component.Type.ToString()); - } - if (component.AccessAppScope) - { - emitter.Emit(new Scalar(nameof(ComponentDefinition.AccessAppScope))); - ValueSerializer!.SerializeValue(emitter, component.AccessAppScope, typeof(bool)); - } - - if (component.CustomProperties != null && component.CustomProperties.Count > 0) - { - emitter.Emit(new Scalar(nameof(ComponentDefinition.CustomProperties))); - ValueSerializer!.SerializeValue(emitter, component.CustomProperties, typeof(IList)); - } - } -} diff --git a/src/Persistence/Yaml/ControlCollectionConverter.cs b/src/Persistence/Yaml/ControlCollectionConverter.cs deleted file mode 100644 index 843719a8..00000000 --- a/src/Persistence/Yaml/ControlCollectionConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.Utilities; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal sealed class ControlCollectionConverter : IYamlTypeConverter -{ - public ControlCollectionConverter() - { - } - - public bool IsTextFirst { get; set; } - - public IValueDeserializer? ValueDeserializer { get; set; } - - public bool Accepts(Type type) - { - return - type == typeof(List) || type.IsSubclassOf(typeof(List)) || - type == typeof(IList) || type.IsSubclassOf(typeof(IList)) || - type == typeof(List) || type.IsSubclassOf(typeof(List)) || - type == typeof(IList) || type.IsSubclassOf(typeof(IList)); - } - - public object? ReadYaml(IParser parser, Type type) - { - if (parser.Current is not SequenceStart) - throw new YamlException(parser.Current!.Start, parser.Current.End, $"Expected sequence start but got {parser.Current.GetType().Name}"); - - if (!parser.MoveNext()) - throw new YamlException(parser.Current.Start, parser.Current.End, "Expected start of control"); - - var controls = new List(); - while (!parser.Accept(out _)) - { - using var serializerState = new SerializerState(); - var controlObj = ValueDeserializer!.DeserializeValue(parser, typeof(Control), serializerState, ValueDeserializer); - if (controlObj == null || controlObj is not Control) - throw new YamlException(parser.Current.Start, parser.Current.End, "Expected control object"); - controls.Add((Control)controlObj); - } - - parser.MoveNext(); - - return controls; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - var property = (ControlProperty)value!; - var style = property.Value.DetermineScalarStyleForProperty(); - -#pragma warning disable CS8604 // Possible null reference with property value, but it's legal in YAML. - emitter.Emit(new Scalar(null, null, property.Value, style, true, false)); -#pragma warning restore CS8604 - } -} diff --git a/src/Persistence/Yaml/ControlConverter.cs b/src/Persistence/Yaml/ControlConverter.cs deleted file mode 100644 index d476db44..00000000 --- a/src/Persistence/Yaml/ControlConverter.cs +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NodeDeserializers; -using YamlDotNet.Serialization.Utilities; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal class ControlConverter : IYamlTypeConverter -{ - private readonly NullNodeDeserializer _nullNodeDeserializer = new(); - protected IControlFactory _controlFactory; - - public ControlConverter(IControlFactory controlFactory) - { - _controlFactory = controlFactory ?? throw new ArgumentNullException(nameof(controlFactory)); - } - - public required YamlSerializationOptions Options { get; set; } - - public IValueDeserializer? ValueDeserializer { get; set; } - - public IValueSerializer? ValueSerializer { get; set; } - - public virtual bool Accepts(Type type) - { - return type == typeof(Control) || type.IsSubclassOf(typeof(Control)); - } - - public virtual object? ReadYaml(IParser parser, Type type) - { - ReadControlDefinitonHeader(parser, out var controlName, out var templateName); - - var controlDefinition = new Dictionary(); - var componentInstanceName = string.Empty; - var componentLibraryUniqueName = string.Empty; - - while (!parser.Accept(out _)) - { - var key = parser.Consume(); - if (controlDefinition.TryGetValue(key.Value, out var _)) - throw new YamlException(key.Start, key.End, $"Duplicate control property '{key.Value}' in control '{controlName}'"); - - // Check if the value is null - object? value = null; - if (_nullNodeDeserializer.Deserialize(parser, typeof(object), null!, out _)) - { - controlDefinition.Add(key.Value, value); - continue; - } - - // Consume known definition - if (key.Value == nameof(Control.Properties)) - { - using var serializerState = new SerializerState(); - value = ValueDeserializer!.DeserializeValue(parser, typeof(ControlPropertiesCollection), serializerState, ValueDeserializer); - } - else if (key.Value == nameof(Control.Children)) - { - using var serializerState = new SerializerState(); - value = ValueDeserializer!.DeserializeValue(parser, typeof(List), serializerState, ValueDeserializer); - } - else if (key.Value == nameof(ComponentDefinition.CustomProperties)) - { - using var serializerState = new SerializerState(); - value = ValueDeserializer!.DeserializeValue(parser, typeof(List), serializerState, ValueDeserializer); - } - else - { - if (parser.Current is Scalar) - { - value = parser.Consume().Value; - - if (key.Value == nameof(Control)) - templateName = (string)value; - else if (key.Value == nameof(ComponentInstance.ComponentName)) - componentInstanceName = (string)value; - else if (key.Value == nameof(ComponentInstance.ComponentLibraryUniqueName)) - componentLibraryUniqueName = (string)value; - else if (key.Value == nameof(Control.Name)) - controlName = (string)value; - } - else - value = ReadKey(parser, key.Value); - } - - controlDefinition.Add(key.Value, value); - } - - if (Options.IsControlIdentifiers) - { - if (string.IsNullOrWhiteSpace(templateName) && string.IsNullOrWhiteSpace(componentInstanceName)) - throw new YamlException(parser.Current!.Start, parser.Current.End, $"Control '{controlName}' doesn't have template name"); - } - - parser.MoveNext(); - if (Options.IsControlIdentifiers) - parser.MoveNext(); - - // Create control instance - var control = _controlFactory.Create( - string.IsNullOrWhiteSpace(controlName) ? templateName : controlName, - templateName, componentInstanceName, componentLibraryUniqueName, controlDefinition); - - return control.AfterDeserialize(_controlFactory); - } - - public void ReadControlDefinitonHeader(IParser parser, out string controlName, out string templateName) - { - if (!parser.MoveNext()) - throw new YamlException(parser.Current!.Start, parser.Current.End, "Expected start of control definition"); - - if (parser.Current is not Scalar scalar) - throw new YamlException(parser.Current!.Start, parser.Current.End, $"Expected control but got {parser.Current.GetType().Name}"); - - controlName = string.Empty; - templateName = string.Empty; - if (Options.IsControlIdentifiers) - { - controlName = scalar.Value; - } - else - { - if (scalar.Value == nameof(Control)) - { - parser.Consume(); - if (parser.Current is not Scalar templateScalar) - throw new YamlException(parser.Current.Start, parser.Current.End, $"Expected control template name or id but got {parser.Current.GetType().Name}"); - templateName = templateScalar.Value; - } - else - { - templateName = scalar.Value; - // Consume empty scalar after template name - parser.Consume(); - } - } - - if (!parser.MoveNext()) - throw new YamlException(parser.Current.Start, parser.Current.End, "Expected begining of control definition"); - - if (Options.IsControlIdentifiers) - { - if (parser.Current is not MappingStart) - throw new YamlException(parser.Current.Start, parser.Current.End, $"Expected control definition but got {parser.Current.GetType().Name}"); - - if (!parser.MoveNext()) - throw new YamlException(parser.Current.Start, parser.Current.End, "Expected start of control definition"); - } - } - - public virtual object? ReadKey(IParser parser, string key) - { - if (parser.Current is MappingStart) - { - using var serializerState = new SerializerState(); - return ValueDeserializer!.DeserializeValue(parser, typeof(object), serializerState, ValueDeserializer); - } - - return null; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - if (value == null) - return; - - var control = ((Control)value).BeforeSerialize(); - if (Options.IsControlIdentifiers) - { - emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, isImplicit: true, MappingStyle.Block)); - emitter.Emit(new Scalar(null, null, control.Name, control.Name.DetermineScalarStyleForProperty(), true, false)); - emitter.Emit(new MappingStart()); - emitter.Emit(nameof(Control), GetControlTemplateName(control)); - } - else - { - emitter.Emit(new MappingStart()); - emitter.Emit(nameof(Control), GetControlTemplateName(control)); - emitter.Emit(nameof(Control.Name), control.Name); - } - - OnWriteAfterName(emitter, control); - - if (Options.IsControlIdentifiers) - emitter.Emit(new MappingEnd()); - emitter.Emit(new MappingEnd()); - } - - public virtual string GetControlTemplateName(Control control) - { - return control.Template.DisplayName; - } - - public virtual void OnWriteAfterName(IEmitter emitter, Control control) - { - emitter.Emit(nameof(Control.Variant), control.Variant); - emitter.Emit(nameof(Control.Layout), control.Layout); - emitter.Emit(nameof(Control.StyleName), control.StyleName); - - if (control.Properties != null && control.Properties.Count > 0) - { - emitter.Emit(new Scalar(nameof(Control.Properties))); - ValueSerializer!.SerializeValue(emitter, control.Properties, typeof(ControlPropertiesCollection)); - } - - if (control.Children != null && control.Children.Count > 0) - { - emitter.Emit(new Scalar(nameof(Control.Children))); - ValueSerializer!.SerializeValue(emitter, control.Children, typeof(IList)); - } - } -} diff --git a/src/Persistence/Yaml/ControlFormatter.cs b/src/Persistence/Yaml/ControlFormatter.cs deleted file mode 100644 index 2cb76a10..00000000 --- a/src/Persistence/Yaml/ControlFormatter.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Globalization; -using Microsoft.PowerPlatform.PowerApps.Persistence.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -/// -/// Utility to handle the changes to the Control pre-serialize and post-deserialize -/// for handling Z-Index, ordering, and nested template. -/// This is intended to be temporary, replaced by the conversion to another lower model. -/// -public static class ControlFormatter -{ - public static T BeforeSerialize(this T control) where T : Control - { - _ = control ?? throw new ArgumentNullException(nameof(control)); - - var childrenToRemove = (control.Children ?? Enumerable.Empty()).Where(c => c.Template.AddPropertiesToParent).ToList(); - var propertiesToMerge = childrenToRemove.SelectMany(c => c.Properties.Where(p => p.Key != PropertyNames.ZIndex)).ToList(); - - var isGroupContainer = control.Template.Name == BuiltInTemplates.GroupContainer.Name; - - // Remove children to be merged - var children = (control.Children ?? Enumerable.Empty()) - .Except(childrenToRemove); - - // Sort by Z-Index (Ascending if child of group container, descending otherwise) - children = (isGroupContainer ? - children.OrderBy(c => c.ZIndex) : - children.OrderByDescending(c => c.ZIndex)) - .Select(BeforeSerialize); // Recurse into child controls - - // Remove the Z-index property and merge in any needed properties from nested data controls - var properties = new ControlPropertiesCollection( - control.Properties - .Where(kvp => kvp.Key != PropertyNames.ZIndex) - .Concat(propertiesToMerge)); - - return control with { Children = children.ToList(), Properties = properties }; - } - - public static T AfterDeserialize(this T control, IControlFactory controlFactory) where T : Control - { - _ = control ?? throw new ArgumentNullException(nameof(control)); - - // No processing needed if there are no children, or if the control is an App - if (control is App || control.Children == null || !control.Children.Any()) - { - return control; - } - - // Create any children from nested templates - var (childrenToAdd, propertiesToRemove) = RestoreNestedTemplates(control, controlFactory); - - var isGroupContainer = control.Template.Name == BuiltInTemplates.GroupContainer.Name; - var originalChildCount = control.Children.Count; - - var children = childrenToAdd - .Concat(control.Children.Select(addZIndex)) - .ToList(); - - var properties = propertiesToRemove.Count > 0 - ? new ControlPropertiesCollection(control.Properties.ExceptBy(propertiesToRemove, kvp => kvp.Key)) - : control.Properties; - - return control with { Children = children, Properties = properties }; - - Control addZIndex(Control control, int orderedIndex) - { - // Controls are sorted in descending Z-Index order, except if they're a child of a group container, in which case it's ascending - var zIndex = originalChildCount - orderedIndex; - if (isGroupContainer) - { - zIndex = orderedIndex + 1; - } - - var zIndexProp = new ControlProperty(PropertyNames.ZIndex, zIndex.ToString(CultureInfo.InvariantCulture)); - control.Properties.Remove(PropertyNames.ZIndex); - - var newProperties = new ControlPropertiesCollection( - control.Properties.Append(KeyValuePair.Create(PropertyNames.ZIndex, zIndexProp))); - - return control with { Properties = newProperties }; - } - } - - private static (List childrenToAdd, List propertiesToRemove) RestoreNestedTemplates(Control control, IControlFactory controlFactory) - { - var childrenToAdd = (control.Template?.NestedTemplates ?? Enumerable.Empty()) - .Where(t => t.AddPropertiesToParent) - .Select(nestedTemplate => - controlFactory.Create(Guid.NewGuid().ToString(), nestedTemplate, - properties: new ControlPropertiesCollection( - nestedTemplate.InputProperties - .Where(prop => control.Properties.ContainsKey(prop.Key)) - .Select(prop => KeyValuePair.Create(prop.Key, control.Properties[prop.Key])))) - ) - .ToList(); - - var propertiesToRemove = childrenToAdd.SelectMany(c => c.Properties.Keys).ToList(); - return (childrenToAdd, propertiesToRemove); - } -} diff --git a/src/Persistence/Yaml/ControlObjectFactory.cs b/src/Persistence/Yaml/ControlObjectFactory.cs deleted file mode 100644 index c1716990..00000000 --- a/src/Persistence/Yaml/ControlObjectFactory.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.ObjectFactories; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public class ControlObjectFactory : IObjectFactory -{ - private readonly DefaultObjectFactory _defaultObjectFactory; - private readonly IControlTemplateStore _controlTemplateStore; - private readonly IControlFactory _controlFactory; - - public ControlObjectFactory(IControlTemplateStore controlTemplateStore, IControlFactory controlFactory) - { - _defaultObjectFactory = new DefaultObjectFactory(); - _controlTemplateStore = controlTemplateStore; - _controlFactory = controlFactory; - } - - public object Create(Type type) - { - if (_controlTemplateStore.TryGetByType(type, out var controlTemplate)) - { - // all fields will be overwritten by the deserializer - return _controlFactory.Create(controlTemplate.Name, controlTemplate); - } - - // Control is abstract, so we'll try to create a concrete custom control type. - if (type == typeof(Control)) - { - return _controlFactory.Create(nameof(CustomControl), nameof(Control)); - } - - return _defaultObjectFactory.Create(type); - } - - public object? CreatePrimitive(Type type) - { - return _defaultObjectFactory.CreatePrimitive(type); - } - - public void ExecuteOnDeserialized(object value) - { - _defaultObjectFactory.ExecuteOnDeserialized(value); - if (value is Control control) - { - RestoreNestedTemplates(control); - } - } - - public void ExecuteOnDeserializing(object value) - { - _defaultObjectFactory.ExecuteOnDeserializing(value); - } - - public void ExecuteOnSerialized(object value) - { - _defaultObjectFactory.ExecuteOnSerialized(value); - } - - public void ExecuteOnSerializing(object value) - { - _defaultObjectFactory.ExecuteOnSerializing(value); - } - - public bool GetDictionary(IObjectDescriptor descriptor, out IDictionary? dictionary, out Type[]? genericArguments) - { - return _defaultObjectFactory.GetDictionary(descriptor, out dictionary, out genericArguments); - } - - public Type GetValueType(Type type) - { - return _defaultObjectFactory.GetValueType(type); - } - - internal void RestoreNestedTemplates(Control control) - { - if (control.Template == null || control.Template.NestedTemplates == null) - return; - - foreach (var nestedTemplate in control.Template.NestedTemplates) - { - if (!nestedTemplate.AddPropertiesToParent) - continue; - - var nestedControl = _controlFactory.Create(Guid.NewGuid().ToString(), nestedTemplate); - control.Children ??= new List(); - control.Children.Add(nestedControl); - - // Move properties from parent to nested template - for (var i = 0; i < control.Properties.Count; i++) - { - var property = control.Properties.ElementAt(i); - if (!nestedTemplate.InputProperties.ContainsKey(property.Key)) - continue; - nestedControl.Properties.Add(property.Key, property.Value); - control.Properties.Remove(property.Key); - i--; - } - } - } -} diff --git a/src/Persistence/Yaml/ControlPropertiesCollectionConverter.cs b/src/Persistence/Yaml/ControlPropertiesCollectionConverter.cs deleted file mode 100644 index 7cb43501..00000000 --- a/src/Persistence/Yaml/ControlPropertiesCollectionConverter.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Collections; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NodeDeserializers; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public class ControlPropertiesCollectionConverter : IYamlTypeConverter -{ - private readonly NullNodeDeserializer _nullNodeDeserializer = new(); - private readonly Scalar NullScalar = new("tag:yaml.org,2002:null", string.Empty); - - public required YamlSerializationOptions Options { get; set; } - - public bool Accepts(Type type) - { - return type == typeof(ControlPropertiesCollection); - } - - public object ReadYaml(IParser parser, Type type) - { - _ = parser ?? throw new ArgumentNullException(nameof(parser)); - - var collection = new ControlPropertiesCollection(); - - parser.MoveNext(); - - while (!parser.Accept(out _)) - { - var key = parser.Consume(); - string? value = null; - if (!_nullNodeDeserializer.Deserialize(parser, typeof(object), null!, out _)) - value = parser.Consume().Value; - - if (Options.IsTextFirst) - collection.Add(key.Value, ControlProperty.FromTextFirstString(key.Value, value)); - else - { - collection.Add(key.Value, value?.TrimStart('=')); - } - } - - parser.MoveNext(); - - return collection; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - var collection = (ControlPropertiesCollection)value!; - - emitter.Emit(new MappingStart()); - - var sortedKeys = collection.Values.OrderBy(p => p, Comparer.Default); - - foreach (var key in sortedKeys) - { - emitter.Emit(new Scalar(key.Name)); - - var property = collection[key.Name]; - var propertyValue = property.Value; - if (Options.IsTextFirst) - { - if (propertyValue == null) - { - emitter.Emit(NullScalar); - continue; - } - - // String values should be quoted and anything else is formula starting with '='. - if (1 < propertyValue.Length && propertyValue.StartsWith('\"') && propertyValue.EndsWith('\"') && !propertyValue.StartsWithOrdinal("\"=")) - { - propertyValue = propertyValue[1..(propertyValue.Length - 1)]; - } - else - propertyValue = $"={propertyValue!}"; - } - else - { - if (propertyValue == null) - { - emitter.Emit(NullScalar); - continue; - } - - propertyValue = propertyValue.StartsWith('=') ? propertyValue : $"={propertyValue}"; - } - - var scalarStyle = propertyValue.DetermineScalarStyleForProperty(); - emitter.Emit(new Scalar(null, null, propertyValue, scalarStyle, true, false)); - } - - emitter.Emit(new MappingEnd()); - } -} diff --git a/src/Persistence/Yaml/ControlPropertyConverter.cs b/src/Persistence/Yaml/ControlPropertyConverter.cs deleted file mode 100644 index 5e80ac7a..00000000 --- a/src/Persistence/Yaml/ControlPropertyConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using YamlDotNet.Core.Events; -using Microsoft.PowerPlatform.PowerApps.Persistence.Extensions; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal sealed class ControlPropertyConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) - { - return type == typeof(ControlProperty); - } - - public object? ReadYaml(IParser parser, Type type) - { - throw new NotImplementedException(); - } - - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - var property = (ControlProperty)value!; - var style = property.Value.DetermineScalarStyleForProperty(); - -#pragma warning disable CS8604 // Possible null reference with property value, but it's legal in YAML. - emitter.Emit(new Scalar(null, null, property.Value, style, true, false)); -#pragma warning restore CS8604 - } -} diff --git a/src/Persistence/Yaml/ControlTypeDiscriminator.cs b/src/Persistence/Yaml/ControlTypeDiscriminator.cs deleted file mode 100644 index cb057c64..00000000 --- a/src/Persistence/Yaml/ControlTypeDiscriminator.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal sealed class ControlTypeDiscriminator : ITypeDiscriminator -{ - private readonly IControlTemplateStore _controlTemplateStore; - - public ControlTypeDiscriminator(IControlTemplateStore controlTemplateStore) - { - _controlTemplateStore = controlTemplateStore ?? throw new ArgumentNullException(nameof(controlTemplateStore)); - } - - public Type BaseType => typeof(object); - - public bool TryDiscriminate(IParser buffer, out Type? suggestedType) - { - suggestedType = null; - - if (!buffer.TryFindMappingEntry(TryFindTemplate, out var scalar, out var value)) - return false; - - // Control is abstract, so we need to return a concrete type. - if (scalar!.Value == YamlFields.Control) - { - var template = ((Scalar)value!).Value.Trim(); - if (_controlTemplateStore.TryGetByIdOrName(template, out var controlTemplate)) - { - // It can be one of the built-in types. - if (_controlTemplateStore.TryGetControlTypeByName(controlTemplate.Name, out var controlType)) - { - suggestedType = controlType; - return true; - } - suggestedType = typeof(BuiltInControl); - return true; - } - - // If we don't have this template, we'll use the custom control type. - suggestedType = typeof(CustomControl); - - return true; - } - - suggestedType = _controlTemplateStore.GetControlType(scalar!.Value); - - return true; - } - - private bool TryFindTemplate(Scalar scalar) - { - if (scalar.Value == YamlFields.Control) - return true; - - if (_controlTemplateStore.Contains(scalar.Value)) - return true; - - return false; - } -} diff --git a/src/Persistence/Yaml/ControlTypeInspector.cs b/src/Persistence/Yaml/ControlTypeInspector.cs deleted file mode 100644 index 73c8955e..00000000 --- a/src/Persistence/Yaml/ControlTypeInspector.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal sealed class ControlTypeInspector : ITypeInspector -{ - private readonly ITypeInspector _innerTypeInspector; - private readonly IControlTemplateStore _controlTemplateStore; - - public ControlTypeInspector(ITypeInspector innerTypeInspector, IControlTemplateStore controlTemplateStore) - { - _innerTypeInspector = innerTypeInspector; - _controlTemplateStore = controlTemplateStore; - } - - public IEnumerable GetProperties(Type type, object? container) - { - var properties = _innerTypeInspector.GetProperties(type, container); - if (_controlTemplateStore.Contains(type)) - return properties.Where(p => !p.Name.Equals(YamlFields.Control, StringComparison.Ordinal)); - - return properties; - } - - public IPropertyDescriptor GetProperty(Type type, object? container, string name, bool ignoreUnmatched) - { - // For built in controls, we'll use the property name as the template name which will be used to get the template. - // for example - // Button: - // Name: "Button1" - if (type == typeof(BuiltInControl)) - { - // for example - // Control: Button - // Name: "Button1" - if (name == YamlFields.Control) - return new TemplatePropertyDescriptor(_controlTemplateStore); - - if (_controlTemplateStore.TryGetTemplateByName(name, out var controlTemplate)) - return new TemplatePropertyDescriptor(_controlTemplateStore, controlTemplate); - return _innerTypeInspector.GetProperty(type, container, name, ignoreUnmatched); - } - - // For custom controls, we expect value to be template id. - // for example - // Control: http://localhost/#customcontrol - // Name: My Custom Control - if (type == typeof(CustomControl) && name == YamlFields.Control) - { - return new TemplatePropertyDescriptor(_controlTemplateStore); - } - - if (_controlTemplateStore.TryGetName(type, out var templateName)) - { - // For built in types, we don't need template id. - // for example - // App: - // Name: My Custom Control - if (name.Equals(templateName, StringComparison.Ordinal)) - return new EmptyPropertyDescriptor(name); - } - - // For controls all properties have to match the list of expected properties. - // This improves error reporting via setting ignoreUnmatched to false. - if (type == typeof(Control)) - return _innerTypeInspector.GetProperty(type, container, name, ignoreUnmatched: false); - - return _innerTypeInspector.GetProperty(type, container, name, ignoreUnmatched); - } -} diff --git a/src/Persistence/Yaml/EmptyPropertyDescriptor.cs b/src/Persistence/Yaml/EmptyPropertyDescriptor.cs deleted file mode 100644 index 3dc85062..00000000 --- a/src/Persistence/Yaml/EmptyPropertyDescriptor.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using YamlDotNet.Core; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public class EmptyPropertyDescriptor : IPropertyDescriptor -{ - public EmptyPropertyDescriptor(string name) - { - Name = name; - } - - public string Name { get; } - - public Type Type => typeof(object); - - public Type? TypeOverride { get; set; } - - public int Order { get; set; } - - public ScalarStyle ScalarStyle { get; set; } - - public bool CanWrite => true; - - public void Write(object? target, object? value) - { - } - - public T? GetCustomAttribute() where T : Attribute - { - return null; - } - - public IObjectDescriptor Read(object? target) - { - return new ObjectDescriptor(target, typeof(object), typeof(object)); - } -} diff --git a/src/Persistence/Yaml/FirstClassControlsEmitter.cs b/src/Persistence/Yaml/FirstClassControlsEmitter.cs deleted file mode 100644 index df87b392..00000000 --- a/src/Persistence/Yaml/FirstClassControlsEmitter.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.EventEmitters; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal sealed class FirstClassControlsEmitter : ChainedEventEmitter -{ - private readonly IControlTemplateStore _controlTemplateStore; - private readonly HashSet _shortNameTypes = new() { "App", "Host", "Screen" }; - - public FirstClassControlsEmitter(IEventEmitter nextEmitter, IControlTemplateStore controlTemplateStore) - : base(nextEmitter) - { - _controlTemplateStore = controlTemplateStore ?? throw new ArgumentNullException(nameof(controlTemplateStore)); - } - - public override void Emit(MappingStartEventInfo eventInfo, IEmitter emitter) - { - nextEmitter.Emit(eventInfo, emitter); - - if (CheckIsFirstClass(eventInfo, out var nodeName)) - { - ObjectDescriptor keySource; - ObjectDescriptor valueSource; - if (_shortNameTypes.Contains(nodeName)) - { - keySource = new ObjectDescriptor(nodeName, typeof(string), typeof(string)); - valueSource = new ObjectDescriptor(null, typeof(string), typeof(string)); - } - else - { - keySource = new ObjectDescriptor(YamlFields.Control, typeof(string), typeof(string)); - valueSource = new ObjectDescriptor(nodeName, typeof(string), typeof(string)); - } - nextEmitter.Emit(new ScalarEventInfo(keySource), emitter); - nextEmitter.Emit(new ScalarEventInfo(valueSource), emitter); - } - } - - private bool CheckIsFirstClass(EventInfo eventInfo, [MaybeNullWhen(false)] out string nodeName) - { - var control = eventInfo.Source.Value as Control; - if (control == null) - { - nodeName = null; - return false; - } - - // If the control has a template, use the template name - if (control.Template != null && control.Template.HasDisplayName) - { - nodeName = control.Template.DisplayName; - return true; - } - - // If template is not found, look for the template by id - if (_controlTemplateStore.TryGetById(control.TemplateId, out var controlTemplate)) - { - nodeName = controlTemplate.DisplayName; - return true; - } - - nodeName = null; - return false; - } -} diff --git a/src/Persistence/Yaml/INamedObject.cs b/src/Persistence/Yaml/INamedObject.cs deleted file mode 100644 index 0c903431..00000000 --- a/src/Persistence/Yaml/INamedObject.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public interface INamedObject -{ - string Name { get; set; } -} diff --git a/src/Persistence/Yaml/IYamlDeserializer.cs b/src/Persistence/Yaml/IYamlDeserializer.cs deleted file mode 100644 index c7744f18..00000000 --- a/src/Persistence/Yaml/IYamlDeserializer.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public interface IYamlDeserializer -{ - /// Thrown when an error occurs while deserializing. - public T? Deserialize(string yaml) where T : notnull; - - /// Thrown when an error occurs while deserializing. - public T? Deserialize(TextReader reader) where T : notnull; -} diff --git a/src/Persistence/Yaml/IYamlSerializationFactory.cs b/src/Persistence/Yaml/IYamlSerializationFactory.cs deleted file mode 100644 index 1507045b..00000000 --- a/src/Persistence/Yaml/IYamlSerializationFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public interface IYamlSerializationFactory -{ - IYamlSerializer CreateSerializer(YamlSerializationOptions? options = null); - - IYamlDeserializer CreateDeserializer(YamlSerializationOptions? options = null); -} diff --git a/src/Persistence/Yaml/IYamlSerializer.cs b/src/Persistence/Yaml/IYamlSerializer.cs deleted file mode 100644 index 88301d3b..00000000 --- a/src/Persistence/Yaml/IYamlSerializer.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public interface IYamlSerializer -{ - /// Thrown when an error occurs while serializing. - public string Serialize(object graph); - - /// Thrown when an error occurs while serializing. - public string SerializeControl(T graph) where T : Control; - - /// Thrown when an error occurs while serializing. - public void SerializeControl(TextWriter writer, T graph) where T : Control; -} diff --git a/src/Persistence/Yaml/NamedObjectEmitter.cs b/src/Persistence/Yaml/NamedObjectEmitter.cs deleted file mode 100644 index d8e2e456..00000000 --- a/src/Persistence/Yaml/NamedObjectEmitter.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using YamlDotNet.Core; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.EventEmitters; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -/// -/// YAML emitter that uses a predicate to determine the node name for a given object. -/// Would result in a YAML node with the resolved name of the object, and the object's value. For example: -/// MyName: -/// Property1: Value1 -/// Property2: Value2 -/// -internal sealed class NamedObjectEmitter : ChainedEventEmitter -{ - private readonly Stack _isNamedObject = new(); - private bool _isName; - - public NamedObjectEmitter(IEventEmitter nextEmitter) : base(nextEmitter) - { - } - - public required YamlSerializationOptions Options { get; set; } - - /// - /// Skip name emission if the object is a named object. - /// - /// - /// - public override void Emit(MappingStartEventInfo eventInfo, IEmitter emitter) - { - nextEmitter.Emit(eventInfo, emitter); - - var namedObject = eventInfo.Source.Value as INamedObject; - _isNamedObject.Push(namedObject != null); - } - - public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) - { - if (_isNamedObject.Peek()) - { - if (eventInfo.Source.Value != null && eventInfo.Source.Value.Equals(nameof(INamedObject.Name))) - { - _isName = true; - return; - } - - // Skip name emission if the object is a named object. - if (_isName) - { - _isName = false; - return; - } - } - nextEmitter.Emit(eventInfo, emitter); - } - - public override void Emit(MappingEndEventInfo eventInfo, IEmitter emitter) - { - _isNamedObject.Pop(); - - nextEmitter.Emit(eventInfo, emitter); - } -} diff --git a/src/Persistence/Yaml/NamedObjectTypeInspector.cs b/src/Persistence/Yaml/NamedObjectTypeInspector.cs deleted file mode 100644 index 4e6847ab..00000000 --- a/src/Persistence/Yaml/NamedObjectTypeInspector.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal sealed class NamedObjectTypeInspector : ITypeInspector -{ - private readonly ITypeInspector _innerTypeInspector; - - public NamedObjectTypeInspector(ITypeInspector innerTypeInspector) - { - _innerTypeInspector = innerTypeInspector; - } - - public IEnumerable GetProperties(Type type, object? container) - { - var properties = _innerTypeInspector.GetProperties(type, container); - if (typeof(INamedObject).IsAssignableFrom(type)) - return properties.Where(p => !p.Name.Equals(nameof(INamedObject.Name), StringComparison.Ordinal)); - - return properties; - } - - public IPropertyDescriptor GetProperty(Type type, object? container, string name, bool ignoreUnmatched) - { - return _innerTypeInspector.GetProperty(type, container, name, ignoreUnmatched); - } -} diff --git a/src/Persistence/Yaml/NamedObjectsCollectionConverter.cs b/src/Persistence/Yaml/NamedObjectsCollectionConverter.cs deleted file mode 100644 index c2c70b3e..00000000 --- a/src/Persistence/Yaml/NamedObjectsCollectionConverter.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.Utilities; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public class NamedObjectsCollectionConverter : IYamlTypeConverter - where T : class, INamedObject -{ - public required YamlSerializationOptions Options { get; set; } - - public IValueDeserializer? ValueDeserializer { get; set; } - - public IValueSerializer? ValueSerializer { get; set; } - - public bool Accepts(Type type) - { - return - type == typeof(List) || type.IsSubclassOf(typeof(List)) || - type == typeof(IList) || type.IsSubclassOf(typeof(IList)) || - type == typeof(T[]); - } - - public object ReadYaml(IParser parser, Type type) - { - if (parser.Current is not SequenceStart) - throw new YamlException(parser.Current!.Start, parser.Current.End, $"Expected sequence of {typeof(T).Name} to start but got {parser.Current.GetType().Name}"); - - if (!parser.MoveNext()) - throw new YamlException(parser.Current.Start, parser.Current.End, $"Expected start of {typeof(T).Name}"); - - var collection = new List(); - while (!parser.Accept(out _)) - { - var name = string.Empty; - if (Options.IsControlIdentifiers) - { - if (parser.Current is not MappingStart) - throw new YamlException(parser.Current!.Start, parser.Current.End, $"Expected mapping of {typeof(T).Name} to start but got {parser.Current.GetType().Name}"); - if (!parser.MoveNext()) - throw new YamlException(parser.Current.Start, parser.Current.End, $"Expected definition of {typeof(T).Name}"); - if (parser.Current is not Scalar) - throw new YamlException(parser.Current!.Start, parser.Current.End, $"Expected name of {typeof(T).Name}"); - - name = parser.Consume().Value; - } - - using var serializerState = new SerializerState(); - var controlObj = ValueDeserializer!.DeserializeValue(parser, typeof(T), serializerState, ValueDeserializer); - if (controlObj is not T namedObject) - throw new YamlException(parser.Current.Start, parser.Current.End, $"Expected {typeof(T).Name}"); - - if (Options.IsControlIdentifiers) - { - namedObject.Name = name; - parser.Consume(); - } - collection.Add(namedObject); - } - - parser.MoveNext(); - - return collection; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type) - { - if (value == null) - return; - - var collection = (IEnumerable)value!; - - emitter.Emit(new SequenceStart(null, null, true, SequenceStyle.Block)); - - foreach (var key in collection) - { - if (Options.IsControlIdentifiers) - { - emitter.Emit(new MappingStart()); - emitter.Emit(new Scalar(key.Name)); - } - - ValueSerializer!.SerializeValue(emitter, key, typeof(T)); - - if (Options.IsControlIdentifiers) - emitter.Emit(new MappingEnd()); - } - - emitter.Emit(new SequenceEnd()); - } -} diff --git a/src/Persistence/Yaml/PropertyNames.cs b/src/Persistence/Yaml/PropertyNames.cs deleted file mode 100644 index 3818120e..00000000 --- a/src/Persistence/Yaml/PropertyNames.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public static class PropertyNames -{ - public const string ZIndex = "ZIndex"; -} diff --git a/src/Persistence/Yaml/TemplatePropertyDescriptor.cs b/src/Persistence/Yaml/TemplatePropertyDescriptor.cs deleted file mode 100644 index 623f7353..00000000 --- a/src/Persistence/Yaml/TemplatePropertyDescriptor.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Core; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public class TemplatePropertyDescriptor : IPropertyDescriptor -{ - private readonly IControlTemplateStore _controlTemplateStore; - private readonly ControlTemplate? _controlTemplate; - - public TemplatePropertyDescriptor(IControlTemplateStore controlTemplateStore, ControlTemplate? controlTemplate = null) - { - _controlTemplateStore = controlTemplateStore; - _controlTemplate = controlTemplate; - } - - public string Name => nameof(Control.Template); - - public Type Type => typeof(object); - - public Type? TypeOverride { get; set; } - - public int Order { get; set; } - - public ScalarStyle ScalarStyle { get; set; } - - public bool CanWrite => true; - - public void Write(object? target, object? value) - { - if (target == null) - return; - - var templateProperty = target.GetType().GetProperty(nameof(Control.Template)) ?? throw new InvalidOperationException($"Target does not have a {nameof(Control.Template)} property."); - if (_controlTemplate == null) - { - if (value is not string val) - throw new InvalidOperationException("Value is not a string."); - - if (_controlTemplateStore.TryGetByIdOrName(val, out var controlTemplate)) - templateProperty.SetValue(target, controlTemplate); - else - templateProperty.SetValue(target, new ControlTemplate(val)); - } - else - templateProperty.SetValue(target, _controlTemplate); - } - - public T? GetCustomAttribute() where T : Attribute - { - return null; - } - - public IObjectDescriptor Read(object? target) - { - return new ObjectDescriptor(target, typeof(object), typeof(object)); - } -} diff --git a/src/Persistence/Yaml/YamlDeserializer.cs b/src/Persistence/Yaml/YamlDeserializer.cs deleted file mode 100644 index 79d056ca..00000000 --- a/src/Persistence/Yaml/YamlDeserializer.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using YamlDotNet.Core; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public class YamlDeserializer : IYamlDeserializer -{ - private readonly IDeserializer _deserializer; - - internal YamlDeserializer(IDeserializer deserializer) - { - _deserializer = deserializer ?? throw new ArgumentNullException(nameof(deserializer)); - } - - public T? Deserialize(string yaml) where T : notnull - { - _ = yaml ?? throw new ArgumentNullException(nameof(yaml)); - - using var reader = new StringReader(yaml); - return DeserializeCore(reader); - } - - public T? Deserialize(TextReader reader) where T : notnull - { - _ = reader ?? throw new ArgumentNullException(nameof(reader)); - - return DeserializeCore(reader); - } - - private T? DeserializeCore(TextReader reader) where T : notnull - { - try - { - return _deserializer.Deserialize(reader); - } - catch (YamlException ex) - { - throw PersistenceLibraryException.FromYamlException(ex, PersistenceErrorCode.DeserializationError); - } - } -} diff --git a/src/Persistence/Yaml/YamlFields.cs b/src/Persistence/Yaml/YamlFields.cs deleted file mode 100644 index 3e24319e..00000000 --- a/src/Persistence/Yaml/YamlFields.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -internal static class YamlFields -{ - public const string Control = "Control"; -} diff --git a/src/Persistence/Yaml/YamlSerializationFactory.cs b/src/Persistence/Yaml/YamlSerializationFactory.cs deleted file mode 100644 index 88f1f3e2..00000000 --- a/src/Persistence/Yaml/YamlSerializationFactory.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.Templates; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public class YamlSerializationFactory : IYamlSerializationFactory -{ - private readonly IControlTemplateStore _controlTemplateStore; - private readonly IControlFactory _controlFactory; - - public YamlSerializationFactory(IControlTemplateStore controlTemplateStore, IControlFactory controlFactory) - { - _controlTemplateStore = controlTemplateStore ?? throw new ArgumentNullException(nameof(controlTemplateStore)); - _controlFactory = controlFactory ?? throw new ArgumentNullException(nameof(controlFactory)); - } - - public IYamlSerializer CreateSerializer(YamlSerializationOptions? options = null) - { - options ??= YamlSerializationOptions.Default; - - var componentConverter = new ComponentConverter(_controlFactory) { Options = options }; - var controlConverter = new ControlConverter(_controlFactory) { Options = options }; - var customPropertiesCollectionConverter = new NamedObjectsCollectionConverter() { Options = options }; - var customPropertyParametersCollectionConverter = new NamedObjectsCollectionConverter() { Options = options }; - - var builder = new SerializerBuilder() - .WithTypeConverter(new ControlPropertiesCollectionConverter() { Options = options }) - .WithTypeConverter(controlConverter) - .WithTypeConverter(componentConverter) - .WithTypeConverter(customPropertiesCollectionConverter) - .WithTypeConverter(customPropertyParametersCollectionConverter) - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitEmptyCollections | DefaultValuesHandling.OmitNull); - - if (options.IsControlIdentifiers) - builder.WithTypeInspector(inner => new NamedObjectTypeInspector(inner)); - else - builder.WithTypeInspector(inner => new ControlTypeInspector(inner, _controlTemplateStore)); - - var valueSerializer = builder.BuildValueSerializer(); - componentConverter.ValueSerializer = valueSerializer; - controlConverter.ValueSerializer = valueSerializer; - customPropertiesCollectionConverter.ValueSerializer = valueSerializer; - customPropertyParametersCollectionConverter.ValueSerializer = valueSerializer; - - return new YamlSerializer(builder.Build()); - } - - public IYamlDeserializer CreateDeserializer(YamlSerializationOptions? options = null) - { - options ??= YamlSerializationOptions.Default; - - var builder = new DeserializerBuilder() - .WithDuplicateKeyChecking() - .IgnoreUnmatchedProperties() - .WithTypeConverter(new ControlPropertiesCollectionConverter() { Options = options }); - - if (!options.IsControlIdentifiers) - { - builder - .WithObjectFactory(new ControlObjectFactory(_controlTemplateStore, _controlFactory)) - .WithTypeDiscriminatingNodeDeserializer(o => - { - o.AddTypeDiscriminator(new ControlTypeDiscriminator(_controlTemplateStore)); - }); - } - - var controlConverter = new ControlConverter(_controlFactory) { Options = options }; - var componentConverter = new ComponentConverter(_controlFactory) { Options = options }; - var appConverter = new AppConverter(_controlFactory) { Options = options }; - var customPropertiesCollectionConverter = new NamedObjectsCollectionConverter() { Options = options }; - var customPropertyParametersCollectionConverter = new NamedObjectsCollectionConverter() { Options = options }; - var controlCollectionConverter = new ControlCollectionConverter() - { - IsTextFirst = options.IsTextFirst - }; - - // Order of type converters is important - builder - .WithTypeConverter(new ControlPropertyConverter()) - .WithTypeConverter(controlConverter) - .WithTypeConverter(componentConverter) - .WithTypeConverter(appConverter) - .WithTypeConverter(customPropertiesCollectionConverter) - .WithTypeConverter(customPropertyParametersCollectionConverter) - .WithTypeConverter(controlCollectionConverter); - - // We need to build the value deserializer after adding the converters - var valueDeserializer = builder.BuildValueDeserializer(); - controlConverter.ValueDeserializer = valueDeserializer; - componentConverter.ValueDeserializer = valueDeserializer; - appConverter.ValueDeserializer = valueDeserializer; - customPropertiesCollectionConverter.ValueDeserializer = valueDeserializer; - customPropertyParametersCollectionConverter.ValueDeserializer = valueDeserializer; - controlCollectionConverter.ValueDeserializer = valueDeserializer; - - return new YamlDeserializer(builder.Build()); - } -} diff --git a/src/Persistence/Yaml/YamlSerializationOptions.cs b/src/Persistence/Yaml/YamlSerializationOptions.cs deleted file mode 100644 index 75202cf3..00000000 --- a/src/Persistence/Yaml/YamlSerializationOptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public record YamlSerializationOptions -{ - [SuppressMessage("Performance", "CA1805:Do not initialize unnecessarily", Justification = "Explicitly setting to false for clarity")] - public bool IsTextFirst { get; init; } = false; - - [SuppressMessage("Performance", "CA1805:Do not initialize unnecessarily", Justification = "Explicitly setting to false for clarity")] - public bool IsControlIdentifiers { get; init; } = false; - - public static readonly YamlSerializationOptions Default = new() - { - IsTextFirst = false, - IsControlIdentifiers = true - }; -} diff --git a/src/Persistence/Yaml/YamlSerializer.cs b/src/Persistence/Yaml/YamlSerializer.cs deleted file mode 100644 index c5c56d66..00000000 --- a/src/Persistence/Yaml/YamlSerializer.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.Models; -using YamlDotNet.Core; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; - -public class YamlSerializer : IYamlSerializer -{ - private readonly ISerializer _serializer; - - internal YamlSerializer(ISerializer serializer) - { - _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - } - - public string Serialize(object graph) - { - _ = graph ?? throw new ArgumentNullException(nameof(graph)); - - return SerializeCore(graph); - } - - public string SerializeControl(T graph) where T : Control - { - _ = graph ?? throw new ArgumentNullException(nameof(graph)); - - return SerializeCore(graph); - } - - public void SerializeControl(TextWriter writer, T graph) where T : Control - { - _ = writer ?? throw new ArgumentNullException(nameof(writer)); - _ = graph ?? throw new ArgumentNullException(nameof(graph)); - - SerializeCore(writer, graph); - } - - private void SerializeCore(TextWriter writer, T graph) - { - try - { - _serializer.Serialize(writer, graph); - } - catch (YamlException ex) - { - throw PersistenceLibraryException.FromYamlException(ex, PersistenceErrorCode.SerializationError); - } - } - - private string SerializeCore(T graph) - { - try - { - return _serializer.Serialize(graph); - } - catch (YamlException ex) - { - throw PersistenceLibraryException.FromYamlException(ex, PersistenceErrorCode.SerializationError); - } - } -} diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json index b9b22b6c..45647be8 100644 --- a/src/Persistence/packages.lock.json +++ b/src/Persistence/packages.lock.json @@ -2,91 +2,17 @@ "version": 1, "dependencies": { "net8.0": { - "Microsoft.Extensions.Logging": { - "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" - } - }, - "Microsoft.PowerFx.Core": { + "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", - "requested": "[1.2.0, )", - "resolved": "1.2.0", - "contentHash": "M9DY6FqWUXUPgjbbHsB3vt7+b4QPFaoKBZ7ixSU7cPgHKzo/aNohzYWxYasXwTV6pamv5gX9AqLuF13RimMPdg==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "6.0.0", - "Microsoft.PowerFx.Transport.Attributes": "1.2.0", - "System.Collections.Immutable": "6.0.0" - } + "requested": "[8.0.2, )", + "resolved": "8.0.2", + "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" }, "YamlDotNet": { "type": "Direct", "requested": "[15.1.6, )", "resolved": "15.1.6", "contentHash": "T/cQEK/KHK96Q8kytJ4iUGDXg1/fj2Qtk6rCQeIlHYU1zTeyGVHW0QNZgREQyxZpygGMDMmrXNWt0sj5TsQnjA==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" - }, - "Microsoft.PowerFx.Transport.Attributes": { - "type": "Transitive", - "resolved": "1.2.0", - "contentHash": "zlvi59W/4MdDJeR2rIL8k633V+FIHtxbhyaurNBZevksNWCZ49AV0bUhpB6cUjD/vwci8ZXXWziw0yYnjXIzNg==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" } } }