From 68a1f10d6e9312cb4823c2277e8c09cbc7e6e94c Mon Sep 17 00:00:00 2001 From: "Joe Mayo (MSFT)" Date: Mon, 10 Feb 2025 13:20:06 -0800 Subject: [PATCH] Update ComponentDefinition OM (#736) - replaced parameter `IsRequired` keyword with `IsOptional`. To ensure smaller serialized yaml. - `PaYamlPropertyDataType` consolidated to replace `PFxDataType` and `PFxFunctionReturnType` to move validation to compiler which will simplify compiler implementation. - change serialized order of `Parameters` to occur last. `NamedObjectCollectionExtensions.EmptyToNull` extension methods. - updated schema documentation - Add additional examples of custom properties --- schemas/pa-yaml/v3.0/pa.schema.yaml | 33 +++++----- .../Serialization/PaYamlSerializerTests.cs | 8 ++- .../Models/NamedObjectCollectionExtensions.cs | 19 ++++++ .../Models/PowerFx/PFxFunctionParameter.cs | 18 ------ .../Models/PowerFx/PFxFunctionReturnType.cs | 20 ------- .../Models/SchemaV3/ComponentDefinition.cs | 22 +++++-- .../PaYamlPropertyDataType.cs} | 8 ++- .../PaYamlSerializationContext.cs | 3 +- .../InputProperties-Default.pa.yaml | 32 ++++++++++ .../Src/Components/MyHeaderComponent.pa.yaml | 12 ++-- .../Src/Components/OutputProperties.pa.yaml | 47 +++++++++++++++ .../Src/Components/Parameters-Default.pa.yaml | 60 +++++++++++++++++++ src/schemas/pa-yaml/v3.0/pa.schema.yaml | 33 +++++----- 13 files changed, 225 insertions(+), 90 deletions(-) create mode 100644 src/Persistence/PaYaml/Models/NamedObjectCollectionExtensions.cs delete mode 100644 src/Persistence/PaYaml/Models/PowerFx/PFxFunctionParameter.cs delete mode 100644 src/Persistence/PaYaml/Models/PowerFx/PFxFunctionReturnType.cs rename src/Persistence/PaYaml/Models/{PowerFx/PFxDataType.cs => SchemaV3/PaYamlPropertyDataType.cs} (59%) create mode 100644 src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/InputProperties-Default.pa.yaml create mode 100644 src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/OutputProperties.pa.yaml create mode 100644 src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/Parameters-Default.pa.yaml diff --git a/schemas/pa-yaml/v3.0/pa.schema.yaml b/schemas/pa-yaml/v3.0/pa.schema.yaml index 5ba35d65..24150087 100644 --- a/schemas/pa-yaml/v3.0/pa.schema.yaml +++ b/schemas/pa-yaml/v3.0/pa.schema.yaml @@ -221,7 +221,7 @@ definitions: Control-Group-name: description: |- The name of the group of controls to associate this control with. - + Groups do not impact the behavior of an app, but are used in the Studio to organize controls when editing. allOf: - { $ref: "#/definitions/Control-instance-name" } @@ -333,22 +333,26 @@ definitions: type: string oneOf: - const: Input - description: An input data property. + description: This kind of property can send or receive values between the app and the component. The formula for this property is defined in the app where the component is used. - const: Output - description: An output data property. + description: This kind of property can send or receive values between the app and the component. The formula for this property is defined once in the component. - const: InputFunction - description: An input function property. + description: This kind of property can be called as a function with parameters. The formula for this property is defined in the app where the component is used. - const: OutputFunction - description: An output function property. + description: This kind of property can be called as a function with parameters. The formula for this property is defined once in the component. - const: Event - description: A property that represents an event. + description: This kind of property allows you to create an event that the component can trigger, and then be handled by the app. - const: Action - description: A property that represents an action. + description: This kind of property can be called as a function with parameters, and can contain logic that changes state (side effects). DisplayName: description: DEPRECATED. This is not used anywhere and will be removed. type: string Description: type: string + Default: + description: The default formula to use for this property when an instance does not explicitly set it. + allOf: + - $ref: "#/definitions/pfx-formula" allOf: - if: properties: @@ -364,10 +368,7 @@ definitions: RaiseOnReset: description: If turned on, the component's OnReset behavior will run when the input property's value changes. type: boolean - Default: - allOf: - - description: The default formula to use for this property when an instance does not explicitly set it. - - $ref: "#/definitions/pfx-formula" + Default: true - if: properties: PropertyKind: { const: "Output" } @@ -390,11 +391,8 @@ definitions: DisplayName: true Description: true ReturnType: { $ref: "#/definitions/pfx-function-return-type" } + Default: true Parameters: { $ref: "#/definitions/pfx-function-parameters" } - Default: - description: The default formula to use for this property when an instance does not explicitly set it. - allOf: - - $ref: "#/definitions/pfx-formula" - if: properties: PropertyKind: { const: "OutputFunction" } @@ -418,6 +416,7 @@ definitions: DisplayName: true Description: true ReturnType: { $ref: "#/definitions/pfx-function-return-type" } + Default: true Parameters: { $ref: "#/definitions/pfx-function-parameters" } - if: properties: @@ -470,7 +469,7 @@ definitions: Parameters: type: object additionalProperties: - type: string + type: string oneOf: - required: [Type] additionalProperties: false @@ -524,7 +523,7 @@ definitions: additionalProperties: false properties: Description: { type: string } - IsRequired: { type: boolean } + IsOptional: { type: boolean } DataType: { $ref: "#/definitions/pfx-data-type" } Default: description: The default formula to use for this parameter when not explicitly specified. diff --git a/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs b/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs index 7767257a..fbc7b1c4 100644 --- a/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs +++ b/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs @@ -3,7 +3,6 @@ using Microsoft.PowerPlatform.PowerApps.Persistence; using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; -using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3; using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; @@ -123,7 +122,7 @@ static int GetGroupCount(IPaControlInstanceContainer container) } [TestMethod] - [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/MyHeaderComponent.pa.yaml", 9, 6, 1)] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/MyHeaderComponent.pa.yaml", 9, 5, 1)] public void DeserializeExamplePaYamlComponentDefinition(string path, int expectedCustomPropertiesCount, int expectedPropertiesCount, int expectedChildrenCount) { var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); @@ -182,6 +181,9 @@ public void DeserializeDuplicateControlNamesShouldFail() [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Screens/ComponentsScreen4.pa.yaml")] [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/MyHeaderComponent.pa.yaml")] [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/MyComponentLibrary.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/InputProperties-Default.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/OutputProperties.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/Parameters-Default.pa.yaml")] [DataRow(@"_TestData/SchemaV3_0/Examples/Single-File-App.pa.yaml")] [DataRow(@"_TestData/SchemaV3_0/Examples/AmbiguousComponentNames.pa.yaml")] [DataRow(@"_TestData/SchemaV3_0/FullSchemaUses/App.pa.yaml")] @@ -274,7 +276,7 @@ public void SerializeComponentCustomPropertyUnionPropertyOrder() PropertyKind = ComponentPropertyKind.Input, DisplayName = "My Property", Description = "My Property Description", - DataType = PFxDataType.Boolean, + DataType = PaYamlPropertyDataType.Boolean, Default = new("true"), }); actualYaml.TrimEnd().Should().Be(""" diff --git a/src/Persistence/PaYaml/Models/NamedObjectCollectionExtensions.cs b/src/Persistence/PaYaml/Models/NamedObjectCollectionExtensions.cs new file mode 100644 index 00000000..c4d746be --- /dev/null +++ b/src/Persistence/PaYaml/Models/NamedObjectCollectionExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; + +public static class NamedObjectCollectionExtensions +{ + public static NamedObjectMapping? EmptyToNull(this NamedObjectMapping? collection) + where TValue : notnull + { + return collection?.Count == 0 ? null : collection; + } + + public static NamedObjectSequence? EmptyToNull(this NamedObjectSequence? collection) + where TValue : notnull + { + return collection?.Count == 0 ? null : collection; + } +} diff --git a/src/Persistence/PaYaml/Models/PowerFx/PFxFunctionParameter.cs b/src/Persistence/PaYaml/Models/PowerFx/PFxFunctionParameter.cs deleted file mode 100644 index 70a1f8af..00000000 --- a/src/Persistence/PaYaml/Models/PowerFx/PFxFunctionParameter.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; - -public record PFxFunctionParameter() -{ - public string? Description { get; init; } - - public bool IsRequired { get; init; } - - public PFxDataType? DataType { get; init; } - - /// - /// The default script for this optional parameter. - /// - public PFxExpressionYaml? Default { get; init; } -} diff --git a/src/Persistence/PaYaml/Models/PowerFx/PFxFunctionReturnType.cs b/src/Persistence/PaYaml/Models/PowerFx/PFxFunctionReturnType.cs deleted file mode 100644 index 7e670158..00000000 --- a/src/Persistence/PaYaml/Models/PowerFx/PFxFunctionReturnType.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; - -public enum PFxFunctionReturnType -{ - Void, - Text, - Number, - Boolean, - DateAndTime, - Screen, - Record, - Table, - Image, - VideoOrAudio, - Color, - Currency, -} diff --git a/src/Persistence/PaYaml/Models/SchemaV3/ComponentDefinition.cs b/src/Persistence/PaYaml/Models/SchemaV3/ComponentDefinition.cs index 6bc9538e..84d239a1 100644 --- a/src/Persistence/PaYaml/Models/SchemaV3/ComponentDefinition.cs +++ b/src/Persistence/PaYaml/Models/SchemaV3/ComponentDefinition.cs @@ -74,14 +74,28 @@ public record ComponentCustomPropertyUnion : ComponentCustomPropertyBase { public bool? RaiseOnReset { get; init; } - public PFxDataType? DataType { get; init; } + public PaYamlPropertyDataType? DataType { get; init; } - public PFxFunctionReturnType? ReturnType { get; init; } - - public NamedObjectSequence Parameters { get; init; } = new(); + public PaYamlPropertyDataType? ReturnType { get; init; } /// /// The default script for this custom input property. /// public PFxExpressionYaml? Default { get; init; } + + public NamedObjectSequence? Parameters { get; init; } +} + +public record ComponentCustomPropertyParameter() +{ + public string? Description { get; init; } + + public bool IsOptional { get; init; } + + public PaYamlPropertyDataType? DataType { get; init; } + + /// + /// The default script for this optional parameter. + /// + public PFxExpressionYaml? Default { get; init; } } diff --git a/src/Persistence/PaYaml/Models/PowerFx/PFxDataType.cs b/src/Persistence/PaYaml/Models/SchemaV3/PaYamlPropertyDataType.cs similarity index 59% rename from src/Persistence/PaYaml/Models/PowerFx/PFxDataType.cs rename to src/Persistence/PaYaml/Models/SchemaV3/PaYamlPropertyDataType.cs index c036f3ff..c4595d35 100644 --- a/src/Persistence/PaYaml/Models/PowerFx/PFxDataType.cs +++ b/src/Persistence/PaYaml/Models/SchemaV3/PaYamlPropertyDataType.cs @@ -1,10 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3; -public enum PFxDataType +public enum PaYamlPropertyDataType { + /// + /// aka Void. Only valid for function return types which are allowed to have side effects. + /// + None, Text, Number, Boolean, diff --git a/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs b/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs index 8e209672..f33ef7dd 100644 --- a/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs +++ b/src/Persistence/PaYaml/Serialization/PaYamlSerializationContext.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3; using YamlDotNet.Core; using YamlDotNet.Serialization; @@ -81,7 +80,7 @@ private void AddTypeConverters(BuilderSkeleton builder) { builder.WithTypeConverter(new PFxExpressionYamlConverter(Options.PFxExpressionYamlFormatting)); builder.WithTypeConverter(new NamedObjectYamlConverter(this)); - builder.WithTypeConverter(new NamedObjectYamlConverter(this)); + builder.WithTypeConverter(new NamedObjectYamlConverter(this)); } // BUG 27469059: Internal classes not accessible to test project. InternalsVisibleTo attribute added to csproj doesn't get emitted because GenerateAssemblyInfo is false. diff --git a/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/InputProperties-Default.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/InputProperties-Default.pa.yaml new file mode 100644 index 00000000..500ae9d0 --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/InputProperties-Default.pa.yaml @@ -0,0 +1,32 @@ +ComponentDefinitions: + MyHeaderComponent: + DefinitionType: CanvasComponent + Description: A component with input properties. + CustomProperties: + MyTextProp1: + PropertyKind: Input + DisplayName: App Title + Description: The title of the App + RaiseOnReset: true + DataType: Text + Default: ="Text" + + MyNumberProp1: + PropertyKind: Input + DataType: Number + Default: =100 + + MyInputFunc1: + PropertyKind: InputFunction + ReturnType: Number + Default: =lhs + rhs + + OnMyEvent1: + PropertyKind: Event + ReturnType: None + # default for void is no formula + Default: = + + Properties: + Fill: =Color.Azure + # Note: Custom input properties do not get any rules stored here. diff --git a/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/MyHeaderComponent.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/MyHeaderComponent.pa.yaml index 7d7f8e82..95c24651 100644 --- a/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/MyHeaderComponent.pa.yaml +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/MyHeaderComponent.pa.yaml @@ -42,11 +42,10 @@ ComponentDefinitions: PropertyKind: InputFunction Description: An input function property ReturnType: Number + Default: =parameter1 + 1 Parameters: - parameter1: DataType: Number - IsRequired: true - Default: =parameter1 + 1 MyOutputFunc2: PropertyKind: OutputFunction @@ -55,16 +54,15 @@ ComponentDefinitions: Parameters: - parameter1: DataType: Number - IsRequired: true MyEvent1None: PropertyKind: Event Description: An event property ReturnType: Number + Default: =1 // comment Parameters: - parameter1: DataType: Number - IsRequired: true MyAction1: PropertyKind: Action @@ -73,11 +71,12 @@ ComponentDefinitions: Parameters: - parameter1: DataType: Number - IsRequired: true - parameter3: + IsOptional: true DataType: Text Default: ="Hello" - param4: + IsOptional: true DataType: Number Properties: @@ -85,8 +84,7 @@ ComponentDefinitions: Height: =50 HeaderTitle: =Concatenate(MyHeaderComponent.AppTitle, " - ", MyHeaderComponent.ScreenTitle) Width: =640 - X: =0 - Y: =0 + MyAction1: =Set(myvar, parameter1) Children: - Label4: diff --git a/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/OutputProperties.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/OutputProperties.pa.yaml new file mode 100644 index 00000000..7aac4233 --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/OutputProperties.pa.yaml @@ -0,0 +1,47 @@ +ComponentDefinitions: + MyHeaderComponent: + DefinitionType: CanvasComponent + Description: A component with input properties. + CustomProperties: + MyTextProp1: + PropertyKind: Output + DisplayName: App Title + Description: The title of the App + DataType: Text + + MyNumberProp1: + PropertyKind: Output + DataType: Number + + MyOutputFunc1: + PropertyKind: OutputFunction + ReturnType: Number + Parameters: + - lhs: + DataType: Number + Default: =100 + - rhs: + IsOptional: true + DataType: Number + Default: =1 + + DoMyAction1: + PropertyKind: Action + ReturnType: None + Parameters: + - newValue: + DataType: Number + Default: =100 + - reason: + IsOptional: true + DataType: Text + Default: ="Text" + + # Note: Custom output properties have their rules stored here + # because they are internal implementation details of each instance. + Properties: + DoMyAction1: = + Fill: =Color.Azure + MyNumberProp1: =100 + MyOutputFunc1: =lhs + rhs + MyTextProp1: ="Text" diff --git a/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/Parameters-Default.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/Parameters-Default.pa.yaml new file mode 100644 index 00000000..78e35730 --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/Parameters-Default.pa.yaml @@ -0,0 +1,60 @@ +ComponentDefinitions: + MyHeaderComponent: + DefinitionType: CanvasComponent + Description: A component with input properties. + CustomProperties: + MyInputFunc1: + PropertyKind: InputFunction + ReturnType: Number + Default: =lhs + rhs + Parameters: + - lhs: + DataType: Number + Default: =100 + - rhs: + IsOptional: true + DataType: Number + Default: =1 + + MyOutputFunc1: + PropertyKind: OutputFunction + ReturnType: Number + Parameters: + - lhs: + DataType: Number + Default: =100 + - rhs: + IsOptional: true + DataType: Number + Default: =1 + + OnMyEvent1: + PropertyKind: Event + ReturnType: None + # default for void is no formula + Default: = + Parameters: + - newValue: + DataType: Number + Default: =100 + - reason: + IsOptional: true + DataType: Text + Default: ="Text" + + DoMyAction1: + PropertyKind: Action + ReturnType: None + Parameters: + - newValue: + DataType: Number + Default: =100 + - reason: + IsOptional: true + DataType: Text + Default: ="Text" + + Properties: + DoMyAction1: = + Fill: =Color.Azure + MyOutputFunc1: =lhs + rhs diff --git a/src/schemas/pa-yaml/v3.0/pa.schema.yaml b/src/schemas/pa-yaml/v3.0/pa.schema.yaml index f031d05d..f2a6890a 100644 --- a/src/schemas/pa-yaml/v3.0/pa.schema.yaml +++ b/src/schemas/pa-yaml/v3.0/pa.schema.yaml @@ -242,7 +242,7 @@ definitions: Control-Group-name: description: |- The name of the group of controls to associate this control with. - + Groups do not impact the behavior of an app, but are used in the Studio to organize controls when editing. allOf: - { $ref: "#/definitions/Control-instance-name" } @@ -359,23 +359,27 @@ definitions: type: string oneOf: - const: Input - description: An input data property. + description: This kind of property can send or receive values between the app and the component. The formula for this property is defined in the app where the component is used. - const: Output - description: An output data property. + description: This kind of property can send or receive values between the app and the component. The formula for this property is defined once in the component. - const: InputFunction - description: An input function property. + description: This kind of property can be called as a function with parameters. The formula for this property is defined in the app where the component is used. - const: OutputFunction - description: An output function property. + description: This kind of property can be called as a function with parameters. The formula for this property is defined once in the component. - const: Event - description: A property that represents an event. + description: This kind of property allows you to create an event that the component can trigger, and then be handled by the app. - const: Action - description: A property that represents an action. + description: This kind of property can be called as a function with parameters, and can contain logic that changes state (side effects). DisplayName: # TODO: This property will get removed from the document description: DEPRECATED. This is not used anywhere and will be removed. type: string Description: type: string + Default: + description: The default formula to use for this property when an instance does not explicitly set it. + allOf: + - $ref: "#/definitions/pfx-formula" allOf: - if: properties: @@ -391,10 +395,7 @@ definitions: RaiseOnReset: description: If turned on, the component's OnReset behavior will run when the input property's value changes. type: boolean - Default: - allOf: - - description: The default formula to use for this property when an instance does not explicitly set it. - - $ref: "#/definitions/pfx-formula" + Default: true - if: properties: PropertyKind: { const: "Output" } @@ -417,11 +418,8 @@ definitions: DisplayName: true Description: true ReturnType: { $ref: "#/definitions/pfx-function-return-type" } + Default: true Parameters: { $ref: "#/definitions/pfx-function-parameters" } - Default: - description: The default formula to use for this property when an instance does not explicitly set it. - allOf: - - $ref: "#/definitions/pfx-formula" - if: properties: PropertyKind: { const: "OutputFunction" } @@ -445,6 +443,7 @@ definitions: DisplayName: true Description: true ReturnType: { $ref: "#/definitions/pfx-function-return-type" } + Default: true Parameters: { $ref: "#/definitions/pfx-function-parameters" } - if: properties: @@ -508,7 +507,7 @@ definitions: Parameters: type: object additionalProperties: - type: string + type: string oneOf: - required: [Type] additionalProperties: false @@ -563,7 +562,7 @@ definitions: additionalProperties: false properties: Description: { type: string } - IsRequired: { type: boolean } + IsOptional: { type: boolean } DataType: { $ref: "#/definitions/pfx-data-type" } Default: description: The default formula to use for this parameter when not explicitly specified.