Skip to content

Commit

Permalink
added YAML (de)serialization (#514)
Browse files Browse the repository at this point in the history
  • Loading branch information
mizrael authored Jan 24, 2024
1 parent fc5b0e1 commit 224d3f1
Show file tree
Hide file tree
Showing 24 changed files with 659 additions and 29 deletions.
4 changes: 3 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<Project>
<Import Project="Versions.props" />

<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Allowed for tests methods")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkVersion)" />
<PackageReference Include="MSTest.TestAdapter" Version="$(MSTestTestAdapterVersion)" />
<PackageReference Include="MSTest.TestFramework" Version="$(MSTestTestFrameworkVersion)" />
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)" />
<PackageReference Include="FluentAssertions" Version="$(FluentAssertionsVersion)" />
</ItemGroup>

<ItemGroup Label="Global usings">
<Using Include="FluentAssertions" />
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
</ItemGroup>

<ItemGroup>
<ProjectReference
Include="..\Microsoft.PowerPlatform.PowerApps.Persistence\Microsoft.PowerPlatform.PowerApps.Persistence.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.PowerPlatform.PowerApps.Persistence.Models;
using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml;

namespace Microsoft.PowerPlatform.PowerApps.Persistence.Tests.Yaml;

[TestClass]
public class YamlDeserializerTests
{
[TestMethod]
public void Deserialize_ShouldParseSimpleStructure()
{
var graph = new Screen()
{
Name = "Screen1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "I am a screen" } },
{ "X", new() { Value = "42" } },
{ "Y", new() { Value = "71" } },
},
};

var serializer = YamlSerializationFactory.CreateSerializer();
var yaml = serializer.Serialize(graph);

var deserializer = YamlSerializationFactory.CreateDeserializer();

var sut = deserializer.Deserialize<Control>(yaml);
sut.Should().NotBeNull().And.BeOfType<Screen>();
sut.Name.Should().Be("Screen1");
sut.ControlUri.Should().Be(BuiltInTemplatesUris.Screen);
sut.Controls.Should().NotBeNull().And.BeEmpty();
sut.Properties.Should().NotBeNull()
.And.HaveCount(3)
.And.ContainKeys("Text", "X", "Y");
sut.Properties["Text"].Value.Should().Be("I am a screen");
sut.Properties["X"].Value.Should().Be("42");
sut.Properties["Y"].Value.Should().Be("71");
}

[TestMethod]
public void Deserialize_ShouldParseYamlWithChildNodes()
{
var graph = new Screen()
{
Name = "Screen1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "I am a screen" } },
},
Controls = new Control[]
{
new Label()
{
Name = "Label1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "lorem ipsum" } },
},
},
new Button()
{
Name = "Button1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "click me" } },
{ "X", new() { Value = "100" } },
{ "Y", new() { Value = "200" } }
},
}
}
};

var serializer = YamlSerializationFactory.CreateSerializer();
var yaml = serializer.Serialize(graph);

var deserializer = YamlSerializationFactory.CreateDeserializer();

var sut = deserializer.Deserialize<Control>(yaml);
sut.Should().NotBeNull().And.BeOfType<Screen>();
sut.Name.Should().Be("Screen1");
sut.ControlUri.Should().Be(BuiltInTemplatesUris.Screen);
sut.Properties.Should().NotBeNull()
.And.HaveCount(1)
.And.ContainKey("Text");
sut.Properties["Text"].Value.Should().Be("I am a screen");

sut.Controls.Should().NotBeNull().And.HaveCount(2);
sut.Controls![0].Should().BeOfType<Label>();
sut.Controls![0].Name.Should().Be("Label1");
sut.Controls![0].ControlUri.Should().Be(BuiltInTemplatesUris.Label);
sut.Controls![0].Properties.Should().NotBeNull()
.And.HaveCount(1)
.And.ContainKey("Text");
sut.Controls![0].Properties["Text"].Value.Should().Be("lorem ipsum");

sut.Controls![1].Should().BeOfType<Button>();
sut.Controls![1].Name.Should().Be("Button1");
sut.Controls![1].ControlUri.Should().Be(BuiltInTemplatesUris.Button);
sut.Controls![1].Properties.Should().NotBeNull()
.And.HaveCount(3)
.And.ContainKeys("Text", "X", "Y");
sut.Controls![1].Properties["Text"].Value.Should().Be("click me");
sut.Controls![1].Properties["X"].Value.Should().Be("100");
sut.Controls![1].Properties["Y"].Value.Should().Be("200");
}

[TestMethod]
public void Deserialize_ShouldParseYamlForCustomControl()
{
var graph = new CustomControl()
{
ControlUri = "http://localhost/#customcontrol",
Name = "CustomControl1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "I am a custom control" } },
},
};

var serializer = YamlSerializationFactory.CreateSerializer();
var yaml = serializer.Serialize(graph);

var deserializer = YamlSerializationFactory.CreateDeserializer();

var sut = deserializer.Deserialize<Control>(yaml);
sut.Should().NotBeNull().And.BeOfType<CustomControl>();
sut.Name.Should().Be("CustomControl1");
sut.ControlUri.Should().Be("http://localhost/#customcontrol");
sut.Controls.Should().NotBeNull().And.BeEmpty();
sut.Properties.Should().NotBeNull()
.And.HaveCount(1)
.And.ContainKey("Text");
sut.Properties["Text"].Value.Should().Be("I am a custom control");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.PowerPlatform.PowerApps.Persistence.Models;
using Microsoft.PowerPlatform.PowerApps.Persistence.Yaml;

namespace Microsoft.PowerPlatform.PowerApps.Persistence.Tests.Yaml;

[TestClass]
public class YamlSerializerTests
{
[TestMethod]
public void Serialize_ShouldCreateValidYamlForSimpleStructure()
{
var graph = new Screen()
{
Name = "Screen1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "I am a screen" } },
},
};

var serializer = YamlSerializationFactory.CreateSerializer();

var sut = serializer.Serialize(graph);
sut.Should().Be($"Screen: {Environment.NewLine}Name: Screen1{Environment.NewLine}Properties:{Environment.NewLine} Text: I am a screen{Environment.NewLine}");
}

[TestMethod]
public void Serialize_ShouldCreateValidYamlWithChildNodes()
{
var graph = new Screen()
{
Name = "Screen1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "I am a screen" } },
},
Controls = new Control[]
{
new Label()
{
Name = "Label1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "lorem ipsum" } },
},
},
new Button()
{
Name = "Button1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "click me" } },
{ "X", new() { Value = "100" } },
{ "Y", new() { Value = "200" } }
},
}
}
};

var serializer = YamlSerializationFactory.CreateSerializer();

var sut = serializer.Serialize(graph);
sut.Should().Be($"Screen: {Environment.NewLine}Name: Screen1{Environment.NewLine}Properties:{Environment.NewLine} Text: I am a screen{Environment.NewLine}Controls:{Environment.NewLine}- Label: {Environment.NewLine} Name: Label1{Environment.NewLine} Properties:{Environment.NewLine} Text: lorem ipsum{Environment.NewLine}- Button: {Environment.NewLine} Name: Button1{Environment.NewLine} Properties:{Environment.NewLine} Text: click me{Environment.NewLine} X: 100{Environment.NewLine} Y: 200{Environment.NewLine}");
}

[TestMethod]
public void Serialize_ShouldCreateValidYamlForCustomControl()
{
var graph = new CustomControl()
{
ControlUri = "http://localhost/#customcontrol",
Name = "CustomControl1",
Properties = new Dictionary<string, ControlPropertyValue>()
{
{ "Text", new() { Value = "I am a custom control" } },
},
};

var serializer = YamlSerializationFactory.CreateSerializer();

var sut = serializer.Serialize(graph);
sut.Should().Be($"Control: http://localhost/#customcontrol{Environment.NewLine}Name: CustomControl1{Environment.NewLine}Properties:{Environment.NewLine} Text: I am a custom control{Environment.NewLine}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.PowerPlatform.PowerApps.Persistence.Models;

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>();
attribute = attributes.FirstOrDefault(a => a is FirstClassAttribute) as FirstClassAttribute;
return attribute is not null;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<!-- Nuget Properties -->
<PropertyGroup>
<PackageId>Microsoft.PowerPlatform.PowerApps.Persistence</PackageId>
<Authors>Microsoft</Authors>
<Company>crmsdk,Microsoft</Company>
<Title>Microsoft Power Platform Canvas App Persistence Library</Title>
<PackageProjectUrl>https://github.com/microsoft/PowerApps-Tooling</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Description>Preview Release</Description>
<PackageReleaseNotes>
Notice:
This package is a preview release - use at your own risk.

We have not stabilized on Namespace or Class names with this package as of yet and things will
change as we move though the preview.

See https://github.com/microsoft/PowerApps-Tooling/releases for the latest release notes.
</PackageReleaseNotes>
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<!-- Workaround for version range https://github.com/NuGet/Home/issues/11842 -->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<NoWarn>$(NoWarn);NU1601</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="YamlDotNet" Version="$(YamlDotNetVersion)" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.PowerPlatform.PowerApps.Persistence.Tests" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models;

internal static class BuiltInTemplatesUris
{
public static readonly Uri Screen = new("http://microsoft.com/appmagic/screen#screen");
public static readonly Uri Button = new("http://microsoft.com/appmagic/button#button");
public static readonly Uri Label = new("http://microsoft.com/appmagic/label#label");
public const string Screen = "http://microsoft.com/appmagic/screen#screen";
public const string Button = "http://microsoft.com/appmagic/button#button";
public const string Label = "http://microsoft.com/appmagic/label#label";
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@

namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models;

[FirstClass(Template)]
internal record Button : Control
[FirstClass(nameof(Button))]
public record Button : Control
{
internal const string Template = "Button";
public Button()
{
ControlUri = BuiltInTemplatesUris.Button;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@

namespace Microsoft.PowerPlatform.PowerApps.Persistence.Models;

//TODO: abstract?
internal record Control
public abstract record Control
{
// TODO: rename to "Control" in yaml
// TODO: make this a string and handle parsing/matchin later
/// <summary>
/// template uri of the control.
/// </summary>
public Uri? ControlUri { get; init; }
public string? ControlUri { get; init; }

/// <summary>
/// the control's name.
Expand All @@ -21,10 +18,10 @@ internal record Control
/// <summary>
/// key/value pairs of Control properties. Mapped to/from Control rules.
/// </summary>
public IReadOnlyDictionary<string, string>? Properties { get; init; }
public ControlPropertiesCollection Properties { get; init; } = new();

/// <summary>
/// list of child controls nested under this control.
/// </summary>
public Control[]? Controls { get; init; }
public Control[]? Controls { get; init; } = Array.Empty<Control>();
}
Loading

0 comments on commit 224d3f1

Please sign in to comment.