Skip to content

Commit

Permalink
PaYaml V3 updates (#679)
Browse files Browse the repository at this point in the history
- add `ScreenInstance.Variant`
- `PaYamlSerializationContext` now holds the `PaYamlSerializerOptions` options instance, and moved YamlDotNet builder configs to this class. This helps to reduce the number of parameters needed for some custom converter operations, along with keeping the options class more like a POCO.
- Used primary ctor in some locations
- PaYaml OM properties for collection properties (`Properties`, `Children`, `Groups`) are now nullable, default to null, to conserve memory all around.
- Reverted type name changes so the term `PaYaml` is preferred over just `Yaml`. As was already discussed as part of original changes, this term is used to differentiate the types from more general purpose Yaml serialization types.
  • Loading branch information
joem-msft authored Jun 25, 2024
1 parent d135c78 commit fed95a2
Show file tree
Hide file tree
Showing 29 changed files with 293 additions and 258 deletions.
1 change: 1 addition & 0 deletions schemas/pa-yaml/v3.0/pa.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ definitions:
type: object
additionalProperties: false
properties:
Variant: { $ref: "#/definitions/Control-variant-name" }
Properties: { $ref: "#/definitions/Properties-formula-map" }
Groups: { $ref: "#/definitions/Groups-of-controls" }
Children: { $ref: "#/definitions/Children-Control-instance-sequence" }
Expand Down
17 changes: 4 additions & 13 deletions src/Persistence.Tests/Extensions/NamedObjectAssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,23 @@ public static NamedObjectAssertions<TValue> Should<TValue>(this NamedObject<TVal
/// Contains a number of methods to assert that a <see cref="NamedObjectMapping{TValue}"/> is in the expected state.
/// </summary>
[DebuggerNonUserCode]
public class NamedObjectAssertions<TValue>
: NamedObjectAssertions<NamedObject<TValue>?, string, TValue, NamedObjectAssertions<TValue>>
public class NamedObjectAssertions<TValue>(NamedObject<TValue>? actualValue)
: NamedObjectAssertions<NamedObject<TValue>?, string, TValue, NamedObjectAssertions<TValue>>(actualValue)
where TValue : notnull
{
public NamedObjectAssertions(NamedObject<TValue>? actualValue)
: base(actualValue)
{
}
}

/// <summary>
/// Contains a number of methods to assert that a <see cref="NamedObjectMapping{TValue}"/> is in the expected state.
/// </summary>
[DebuggerNonUserCode]
public class NamedObjectAssertions<TNamedObject, TName, TValue, TAssertions>
: ReferenceTypeAssertions<TNamedObject, TAssertions>
public class NamedObjectAssertions<TNamedObject, TName, TValue, TAssertions>(TNamedObject actualValue)
: ReferenceTypeAssertions<TNamedObject, TAssertions>(actualValue)
where TNamedObject : INamedObject<TName, TValue>?
where TName : notnull
where TValue : notnull
where TAssertions : NamedObjectAssertions<TNamedObject, TName, TValue, TAssertions>
{
public NamedObjectAssertions(TNamedObject actualValue)
: base(actualValue)
{
}

protected override string Identifier => "NamedObject";

public AndConstraint<TAssertions> HaveValueEqual(TValue expected, string because = "", params object[] becauseArgs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,22 @@ public static NamedObjectCollectionAssertions<TValue> Should<TValue>(this IReadO
/// Contains a number of methods to assert that a <see cref="NamedObjectMapping{TValue}"/> is in the expected state.
/// </summary>
[DebuggerNonUserCode]
public class NamedObjectCollectionAssertions<TValue>
: NamedObjectCollectionAssertions<IReadOnlyNamedObjectCollection<TValue>?, TValue, NamedObjectCollectionAssertions<TValue>>
public class NamedObjectCollectionAssertions<TValue>(IReadOnlyNamedObjectCollection<TValue>? actualValue)
: NamedObjectCollectionAssertions<IReadOnlyNamedObjectCollection<TValue>?, TValue, NamedObjectCollectionAssertions<TValue>>(actualValue)
where TValue : notnull
{
public NamedObjectCollectionAssertions(IReadOnlyNamedObjectCollection<TValue>? actualValue)
: base(actualValue)
{
}
}

/// <summary>
/// Contains a number of methods to assert that a <see cref="NamedObjectMapping{TValue}"/> is in the expected state.
/// </summary>
[DebuggerNonUserCode]
public class NamedObjectCollectionAssertions<TCollection, TValue, TAssertions>
: GenericCollectionAssertions<TCollection, NamedObject<TValue>, TAssertions>
public class NamedObjectCollectionAssertions<TCollection, TValue, TAssertions>(TCollection actualValue)
: GenericCollectionAssertions<TCollection, NamedObject<TValue>, TAssertions>(actualValue)
where TCollection : IReadOnlyNamedObjectCollection<TValue>?
where TValue : notnull
where TAssertions : NamedObjectCollectionAssertions<TCollection, TValue, TAssertions>
{
public NamedObjectCollectionAssertions(TCollection actualValue)
: base(actualValue)
{
}

public AndConstraint<TAssertions> ContainNames(params string[] expected)
{
return ContainNames(expected, string.Empty);
Expand Down Expand Up @@ -98,16 +89,12 @@ public WhoseNamedObjectConstraint<TCollection, TValue, TAssertions> ContainName(
}

[DebuggerNonUserCode]
public class WhoseNamedObjectConstraint<TCollection, TValue, TAssertions> : AndConstraint<TAssertions>
public class WhoseNamedObjectConstraint<TCollection, TValue, TAssertions>(TAssertions parentConstraint, NamedObject<TValue> namedObject)
: AndConstraint<TAssertions>(parentConstraint)
where TCollection : IReadOnlyNamedObjectCollection<TValue>?
where TValue : notnull
where TAssertions : NamedObjectCollectionAssertions<TCollection, TValue, TAssertions>
{
public WhoseNamedObjectConstraint(TAssertions parentConstraint, NamedObject<TValue> namedObject) : base(parentConstraint)
{
WhoseNamedObject = namedObject;
}

public NamedObject<TValue> WhoseNamedObject { get; }
public NamedObject<TValue> WhoseNamedObject { get; } = namedObject;
public TValue WhoseValue => WhoseNamedObject.Value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using FluentAssertions.Primitives;
using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models;

namespace Persistence.Tests.Extensions;

[DebuggerNonUserCode]
public static class PaYamlLocationAssertionExtensions
{
public static PaYamlLocationAssertions Should(this PaYamlLocation? actualValue)
{
return new(actualValue);
}
}

[DebuggerNonUserCode]
public class PaYamlLocationAssertions(PaYamlLocation? actualValue)
: ObjectAssertions<PaYamlLocation?, PaYamlLocationAssertions>(actualValue)
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public static void ShouldNotBeNull<T>([NotNull] this T? value)
}
}

/// <summary>
/// Asserts that the specified object does not define a member with the specified name.
/// This is useful to document tests which are written given that some future expected member is not yet defined.
/// </summary>
public static AndConstraint<TAssertions> NotDefineMember<TSubject, TAssertions>(this ObjectAssertions<TSubject, TAssertions> assertions, string memberName, string because = "", params object[] becauseArgs)
where TAssertions : ObjectAssertions<TSubject, TAssertions>
where TSubject : class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ public void DescendantControlInstancesSingleLevel()
{
var screen = new NamedObject<ScreenInstance>("Screen1", new()
{
Children =
{
Children = [
new("Ctrl0", new("GroupContainer")),
new("Ctrl1", new("GroupContainer")),
new("Ctrl2", new("GroupContainer")),
},
],
});

screen.DescendantControlInstances().SelectNames().Should().Equal(new[]
Expand All @@ -35,22 +34,17 @@ public void DescendantControlInstances1AtEachLevel()
{
var screen = new NamedObject<ScreenInstance>("Screen1", new()
{
Children =
{
Children = [
new("Ctrl0", new("GroupContainer")
{
Children =
{
Children = [
new("Ctrl0.0", new("GroupContainer")
{
Children =
{
new("Ctrl0.0.0", new("GroupContainer")),
}
Children = [new("Ctrl0.0.0", new("GroupContainer"))]
}),
}
]
}),
},
],
});

screen.DescendantControlInstances().SelectNames().Should().Equal(new[]
Expand All @@ -66,41 +60,36 @@ public void DescendantControlInstancesMultiLevelTest()
{
var screen = new NamedObject<ScreenInstance>("Screen1", new()
{
Children =
{
Children = [
new("Ctrl0", new("GroupContainer")
{
Children =
{
Children = [
new("Ctrl0.0", new("Label")),
new("Ctrl0.1", new("Label")),
},
],
}),
new("Ctrl1", new("GroupContainer")
{
Children =
{
Children = [
new("Ctrl1.0", new("Label")),
new("Ctrl1.1", new("GroupContainer")
{
Children =
{
Children = [
new("Ctrl1.1.0", new("Label")),
new("Ctrl1.1.1", new("Label")),
},
],
}),
new("Ctrl1.2", new("Label")),
},
],
}),
new("Ctrl2", new("GroupContainer")
{
Children =
{
Children = [
new("Ctrl2.0", new("Label")),
new("Ctrl2.1", new("Label")),
},
],
}),
},
],
});

screen.DescendantControlInstances().SelectNames().Should().Equal(new[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ namespace Persistence.Tests.PaYaml.Serialization;
[TestClass]
public class NamedObjectSequenceSerializationTests : SerializationTestBase
{
protected override void ConfigureYamlDotNetDeserializer(DeserializerBuilder builder, PaYamlSerializerOptions options, SerializationContext serializationContext)
protected override void ConfigureYamlDotNetDeserializer(DeserializerBuilder builder, PaYamlSerializationContext context)
{
base.ConfigureYamlDotNetDeserializer(builder, options, serializationContext);
builder.WithTypeConverter(new NamedObjectYamlConverter<string>(serializationContext));
base.ConfigureYamlDotNetDeserializer(builder, context);
builder.WithTypeConverter(new NamedObjectYamlConverter<string>(context));
}

protected override void ConfigureYamlDotNetSerializer(SerializerBuilder builder, PaYamlSerializerOptions options, SerializationContext serializationContext)
protected override void ConfigureYamlDotNetSerializer(SerializerBuilder builder, PaYamlSerializationContext context)
{
base.ConfigureYamlDotNetSerializer(builder, options, serializationContext);
builder.WithTypeConverter(new NamedObjectYamlConverter<string>(serializationContext));
base.ConfigureYamlDotNetSerializer(builder, context);
builder.WithTypeConverter(new NamedObjectYamlConverter<string>(context));
}

[TestMethod]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ public PFxExpressionYamlConverterTests()
};
}

protected override void ConfigureYamlDotNetDeserializer(DeserializerBuilder builder, PaYamlSerializerOptions options, SerializationContext serializationContext)
protected override void ConfigureYamlDotNetDeserializer(DeserializerBuilder builder, PaYamlSerializationContext context)
{
base.ConfigureYamlDotNetDeserializer(builder, options, serializationContext);
builder.WithTypeConverter(new PFxExpressionYamlConverter(options.PFxExpressionYamlFormatting));
base.ConfigureYamlDotNetDeserializer(builder, context);
builder.WithTypeConverter(new PFxExpressionYamlConverter(context.Options.PFxExpressionYamlFormatting));
}

protected override void ConfigureYamlDotNetSerializer(SerializerBuilder builder, PaYamlSerializerOptions options, SerializationContext serializationContext)
protected override void ConfigureYamlDotNetSerializer(SerializerBuilder builder, PaYamlSerializationContext context)
{
base.ConfigureYamlDotNetSerializer(builder, options, serializationContext);
builder.WithTypeConverter(new PFxExpressionYamlConverter(options.PFxExpressionYamlFormatting));
base.ConfigureYamlDotNetSerializer(builder, context);
builder.WithTypeConverter(new PFxExpressionYamlConverter(context.Options.PFxExpressionYamlFormatting));
}

[TestMethod]
Expand Down
55 changes: 43 additions & 12 deletions src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,30 @@ namespace Persistence.Tests.PaYaml.Serialization;
[TestClass]
public class PaYamlSerializerTests : VSTestBase
{
[TestMethod]
public void DeserializeNamedObjectSetsLocationInfo()
{
// Since App.Properties is a NamedObjectMapping, the location info should be set on the NamedObject
var paModule = PaYamlSerializer.Deserialize<PaModule>("""
App:
Properties:
Foo: =true
Bar: ="hello world"
""");
paModule.ShouldNotBeNull();
paModule.App.ShouldNotBeNull();
paModule.App.Properties.ShouldNotBeNull();
paModule.App.Properties.Should().ContainName("Foo").WhoseNamedObject.Start.Should().Be(new(3, 9));
paModule.App.Properties.Should().ContainName("Bar").WhoseNamedObject.Start.Should().Be(new(4, 9));
}

#region Deserialize Examples

[TestMethod]
[DataRow(@"_TestData/SchemaV3_0/Examples/Src/App.pa.yaml", 5)]
public void DeserializeExamplePaYamlApp(string path, int expectedAppPropertiesCount)
{
var paFileRoot = YamlSerializer.Deserialize<PaModule>(File.ReadAllText(path));
var paFileRoot = PaYamlSerializer.Deserialize<PaModule>(File.ReadAllText(path));
paFileRoot.ShouldNotBeNull();

// Top level properties
Expand All @@ -34,7 +51,7 @@ public void DeserializeExamplePaYamlApp(string path, int expectedAppPropertiesCo
[DataRow(@"_TestData/SchemaV3_0/Examples/Src/Screens/ComponentsScreen4.pa.yaml", 0, 6, 6, 0, 0)]
public void DeserializeExamplePaYamlScreen(string path, int expectedScreenPropertiesCount, int expectedScreenChildrenCount, int expectedDescendantsCount, int expectedScreenGroupsCount, int expectedTotalGroupsCount)
{
var paFileRoot = YamlSerializer.Deserialize<PaModule>(File.ReadAllText(path));
var paFileRoot = PaYamlSerializer.Deserialize<PaModule>(File.ReadAllText(path));
paFileRoot.ShouldNotBeNull();

// Top level properties
Expand All @@ -45,19 +62,33 @@ public void DeserializeExamplePaYamlScreen(string path, int expectedScreenProper
// Check screen counts
paFileRoot.Screens.Should().HaveCount(1);
var screen = paFileRoot.Screens.First().Value;
screen.Properties.Should().HaveCount(expectedScreenPropertiesCount);
screen.Children.Should().HaveCount(expectedScreenChildrenCount);
screen.DescendantControlInstances().Should().HaveCount(expectedDescendantsCount);

screen.Groups.Should().HaveCount(expectedScreenGroupsCount);
screen.DescendantControlInstances().SelectMany(nc => nc.Value.Groups).Should().HaveCount(expectedTotalGroupsCount - expectedScreenGroupsCount);
if (expectedScreenPropertiesCount == 0)
screen.Properties.Should().BeNull();
else
screen.Properties.Should().HaveCount(expectedScreenPropertiesCount);

if (expectedScreenChildrenCount == 0)
screen.Children.Should().BeNull();
else
screen.Children.Should().HaveCount(expectedScreenChildrenCount);

if (expectedDescendantsCount == 0)
screen.Properties.Should().BeNull();
else
screen.DescendantControlInstances().Should().HaveCount(expectedDescendantsCount);

if (expectedScreenGroupsCount == 0)
screen.Properties.Should().BeNull();
else
screen.Groups.Should().HaveCount(expectedScreenGroupsCount);
screen.DescendantControlInstances().SelectMany(nc => nc.Value.Groups ?? []).Should().HaveCount(expectedTotalGroupsCount - expectedScreenGroupsCount);
}

[TestMethod]
[DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/MyHeaderComponent.pa.yaml", 9, 6, 1)]
public void DeserializeExamplePaYamlComponentDefinition(string path, int expectedCustomPropertiesCount, int expectedPropertiesCount, int expectedChildrenCount)
{
var paFileRoot = YamlSerializer.Deserialize<PaModule>(File.ReadAllText(path));
var paFileRoot = PaYamlSerializer.Deserialize<PaModule>(File.ReadAllText(path));
paFileRoot.ShouldNotBeNull();

// Top level properties
Expand All @@ -77,7 +108,7 @@ public void DeserializeExamplePaYamlComponentDefinition(string path, int expecte
public void DeserializeExamplePaYamlSingleFileApp()
{
var path = @"_TestData/SchemaV3_0/Examples/Single-File-App.pa.yaml";
var paFileRoot = YamlSerializer.Deserialize<PaModule>(File.ReadAllText(path));
var paFileRoot = PaYamlSerializer.Deserialize<PaModule>(File.ReadAllText(path));
paFileRoot.ShouldNotBeNull();

// Top level properties
Expand Down Expand Up @@ -112,10 +143,10 @@ public void DeserializeExamplePaYamlSingleFileApp()
public void RoundTripFromYaml(string path)
{
var originalYaml = File.ReadAllText(path);
var paFileRoot = YamlSerializer.Deserialize<PaModule>(originalYaml);
var paFileRoot = PaYamlSerializer.Deserialize<PaModule>(originalYaml);
paFileRoot.ShouldNotBeNull();

var roundTrippedYaml = YamlSerializer.Serialize(paFileRoot);
var roundTrippedYaml = PaYamlSerializer.Serialize(paFileRoot);
TestContext.WriteTextWithLineNumbers(roundTrippedYaml, "roundTrippedYaml:");
roundTrippedYaml.Should().BeYamlEquivalentTo(originalYaml);
}
Expand Down
Loading

0 comments on commit fed95a2

Please sign in to comment.