Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions nuget.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@
<configuration>
<packageSources>
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="azure-default" value="https://packagefeedproxy.microsoft.io/nuget/v3/index.json" protocolVersion="3" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
<!--
When CentralPackageManagement is enabled, the packageSourceMapping element helps to remove NU1507 errors.
See: https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping#enable-by-manually-editing-nugetconfig
-->
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
<packageSource key="nuget">
<packageSource key="azure-default">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
<disabledPackageSources>
<add key="nuget.org" value="true" />
</disabledPackageSources>
</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ namespace Persistence.Tests.PaYaml.Serialization;
[TestClass]
public class PaYamlSerializerTests : VSTestBase
{
[TestMethod]
public void DeserializeAppInstanceSetsLocationInfo()
{
PaYamlSerializer.Deserialize<PaModule>("App: {}")!.App!.Start.Should().Be(new(1, 6));
PaYamlSerializer.Deserialize<PaModule>("\nApp: {}")!.App!.Start.Should().Be(new(2, 6));
PaYamlSerializer.Deserialize<PaModule>("\nApp:\n Properties: {}")!.App!.Start.Should().Be(new(3, 2));
}

[TestMethod]
public void DeserializeNamedObjectSetsLocationInfo()
{
Expand All @@ -27,6 +35,7 @@ public void DeserializeNamedObjectSetsLocationInfo()
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));
paModule.App.Start.Should().Be(new(2, 5), "`App.Start` location info should now be set correctly");
}

[TestMethod]
Expand Down
4 changes: 2 additions & 2 deletions src/Persistence/Extensions/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.Extensions;
public static class JsonExtensions
{
/// <summary>
/// A fluent way of making a <see cref="JsonSerializerOptions"/> instance immutable.
/// A fluent way of calling <see cref="JsonSerializerOptions.MakeReadOnly()"/> to make a <see cref="JsonSerializerOptions"/> instance immutable.
/// Especially useful for shared static instances.
/// </summary>
public static JsonSerializerOptions ToReadOnly(this JsonSerializerOptions options)
public static JsonSerializerOptions MakeReadOnlyFluent(this JsonSerializerOptions options)
{
options.MakeReadOnly(populateMissingResolver: true);
return options;
Expand Down
8 changes: 4 additions & 4 deletions src/Persistence/MsApp/Serialization/MsappSerialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ public static class MsappSerialization
// But this may have impact on other code which depends on this property.
new JsonDateTimeAssumesUtcConverter(),
},
}.ToReadOnly();
}.MakeReadOnlyFluent();

internal static readonly JsonSerializerOptions DocumentJsonSerializeOptions = new JsonSerializerOptions(DefaultSharedJsonSerializeOptions)
.ToReadOnly();
.MakeReadOnlyFluent();

/// <summary>
/// This should match the options used in DocumentServer for deserializing msapp json files.
Expand All @@ -51,7 +51,7 @@ public static class MsappSerialization
{
// Note: The docsvr doesn't indent the Header.json file.
WriteIndented = false,
}.ToReadOnly();
}.MakeReadOnlyFluent();

/// <summary>
/// Serialization options used for the 'packed.json' file in the msapp archive.
Expand All @@ -76,5 +76,5 @@ public static class MsappSerialization
// In order to ensure forward-compatible deserialization, we ignore unknown members
// Any object model that wants to also survive round-tripping, must use JsonExtensionData to capture those unknown members.
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
}.ToReadOnly();
}.MakeReadOnlyFluent();
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ public static class MsaprSerialization
// In order to ensure forward-compatible deserialization, we ignore unknown members
// Any object model that wants to also survive round-tripping, must use JsonExtensionData to capture those unknown members.
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
}.ToReadOnly();
}.MakeReadOnlyFluent();
}
16 changes: 16 additions & 0 deletions src/Persistence/PaYaml/Models/IMayHavePaYamlLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models;

/// <summary>
/// An interface for objects that may have a YAML location.
/// This is used to keep a consistent API for objects that may have a location in the YAML file.
/// </summary>
public interface IMayHavePaYamlLocation
{
/// <summary>
/// The location in the YAML file where this object starts.
/// </summary>
PaYamlLocation? Start { get; }
}
12 changes: 12 additions & 0 deletions src/Persistence/PaYaml/Models/ISetPaYamlNodeLocation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models;

internal interface ISetPaYamlNodeLocation : IMayHavePaYamlLocation
{
/// <summary>
/// Sets the location of the node in the YAML file.
/// </summary>
void SetNodeLocation(PaYamlLocation? start);
}
9 changes: 7 additions & 2 deletions src/Persistence/PaYaml/Models/SchemaV3/AppInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@

namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3;

public record AppInstance
public record AppInstance : IMayHavePaYamlLocation, ISetPaYamlNodeLocation
{
[YamlIgnore]
public PaYamlLocation? Start { get; init; }
public PaYamlLocation? Start { get; private set; }

public NamedObjectMapping<PFxExpressionYaml>? Properties { get; init; }

void ISetPaYamlNodeLocation.SetNodeLocation(PaYamlLocation? start)
{
Start = start;
}

// WorkItem 27966436: Support saving AppHost instances to top-level property 'App'
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization.NodeDeserializers;

namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization;

Expand All @@ -29,6 +30,7 @@ internal void ApplyToDeserializerBuilder(DeserializerBuilder builder)
builder
.WithDuplicateKeyChecking()
.IgnoreFields()
.WithSetNodeLocationNodeDeserializerWrapper<ObjectNodeDeserializer>()
;

if (Options.MaximumRecursion.HasValue)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models;
using YamlDotNet.Core;
using YamlDotNet.Serialization;

namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization;

/// <summary>
/// A node deserializer that wraps an existing <see cref="INodeDeserializer"/> which will set the location information on the value if it implements <see cref="IMayHavePaYamlLocation"/>.
/// </summary>
internal sealed class SetNodeLocationNodeDeserializerWrapper<TNodeDeserializerToWrap>(TNodeDeserializerToWrap wrappedNodeDeserializer) : INodeDeserializer
where TNodeDeserializerToWrap : INodeDeserializer
{
public bool Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer)
{
// Capture the start location before deserializing the node
var start = reader.Current?.Start;
if (wrappedNodeDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value, rootDeserializer))
{
if (start != null && value is ISetPaYamlNodeLocation setNodeLocationValue)
{
// Only set the location if it hasn't been set yet
if (setNodeLocationValue.Start is null)
{
setNodeLocationValue.SetNodeLocation(PaYamlLocation.FromMark(start.Value));
}
}

return true;
}

return false;
}
}

internal static class SetNodeLocationNodeDeserializerWrapperExtensions
{
/// <summary>
/// Wraps the specified <typeparamref name="TNodeDeserializerToWrap"/> with a <see cref="SetNodeLocationNodeDeserializerWrapper{TNodeDeserializerToWrap}"/> which will set the location information on the value if it implements <see cref="IMayHavePaYamlLocation"/>.
/// </summary>
/// <typeparam name="TNodeDeserializerToWrap">The type of the <see cref="INodeDeserializer"/> to be replaced and wrapped.</typeparam>
public static DeserializerBuilder WithSetNodeLocationNodeDeserializerWrapper<TNodeDeserializerToWrap>(this DeserializerBuilder builder)
where TNodeDeserializerToWrap : INodeDeserializer
{
_ = builder ?? throw new ArgumentNullException(nameof(builder));

return builder.WithNodeDeserializer(
baseDeserializer => new SetNodeLocationNodeDeserializerWrapper<TNodeDeserializerToWrap>((TNodeDeserializerToWrap)baseDeserializer),
s => s.InsteadOf<TNodeDeserializerToWrap>());
}
}