Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add serialization of "items" less arrays #2850

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,13 @@ private async Task WritePropertyAsync(string propertyName, ISwaggerSchema proper
return;
}

if (propertySchema == null)
if (propertySchema == null && !_schemaLessBody)
{
throw new PowerFxConnectorException($"Missing schema for property {propertyName}");
}

// if connector has null as a type but "array" is provided, let's write it down. this is possible in case of x-ms-dynamic-properties
if (fv is TableValue tableValue && ((propertySchema.Type ?? "array") == "array"))
if (fv is TableValue tableValue && ((propertySchema?.Type ?? "array") == "array"))
{
StartArray(propertyName);

Expand All @@ -169,12 +169,18 @@ private async Task WritePropertyAsync(string propertyName, ISwaggerSchema proper
RecordValue rva = item.Value;

// If we have an object schema, we will try to follow it
if (propertySchema.Items?.Type == "object" || propertySchema.Items?.Type == "array")
if (propertySchema?.Items?.Type == "object" || propertySchema?.Items?.Type == "array")
{
await WritePropertyAsync(null, propertySchema.Items, rva).ConfigureAwait(false);
continue;
}

if (propertySchema?.Items?.Type == null && _schemaLessBody)
{
await WritePropertyAsync(null, null, rva).ConfigureAwait(false);
continue;
}

// Else, we write primitive types only
if (rva.Fields.Count() != 1)
{
Expand All @@ -193,6 +199,64 @@ private async Task WritePropertyAsync(string propertyName, ISwaggerSchema proper
return;
}

if (_schemaLessBody && propertySchema?.Type == null)
{
if (propertyName != null)
{
WritePropertyName(propertyName);
}

if (fv is NumberValue numberValue)
{
WriteNumberValue(numberValue.Value);
}
else if (fv is DecimalValue decimalValue)
{
WriteDecimalValue(decimalValue.Value);
}
else if (fv is BooleanValue booleanValue)
{
WriteBooleanValue(booleanValue.Value);
}
else if (fv is StringValue stringValue)
{
WriteStringValue(stringValue.Value);
}
else if (fv is DateTimeValue dtv)
{
WriteDateTimeValue(_utcConverter.ToUTC(dtv));
}
else if (fv is DateValue dv)
{
WriteDateValue(dv.GetConvertedValue(null));
}
else if (fv is BlobValue bv)
{
await WriteBlobValueAsync(bv).ConfigureAwait(false);
}
else if (fv is OptionSetValue optionSetValue)
{
WriteStringValue(optionSetValue.Option);
}
else if (fv is RecordValue recordValue)
{
StartObject();

foreach (NamedValue field in recordValue.Fields)
{
await WritePropertyAsync(field.Name, null, field.Value).ConfigureAwait(false);
}

EndObject();
}
else
{
throw new NotImplementedException($"Unsupported type {fv.GetType().FullName} for schemaless body");
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we share this code with somewhere else?
It seems especially easy to miss a case.

return;
}

switch (propertySchema.Type)
{
case "null":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,88 @@ public async Task DataverseTest_WithComplexMapping()
Assert.Equal(expected, actual);
}

[Fact]
public async Task DataverseTest_WithItemLessArray()
{
using var testConnector = new LoggingTestServer(@"Swagger\Dataverse.json", _output);
using var httpClient = new HttpClient(testConnector);
using PowerPlatformConnectorClient client = new PowerPlatformConnectorClient("https://tip1002-002.azure-apihub.net", "b29c41cf-173b-e469-830b-4f00163d296b" /* environment Id */, "82728ddb6bfa461ea3e50e17da8ab164" /* connectionId */, () => "eyJ0eXAiOiJKV1QiLCJ...", httpClient) { SessionId = "a41bd03b-6c3c-4509-a844-e8c51b61f878" };

BaseRuntimeConnectorContext runtimeContext = new TestConnectorRuntimeContext("DV", client, console: _output);
ConnectorFunction[] functions = OpenApiParser.GetFunctions(new ConnectorSettings("DV") { Compatibility = ConnectorCompatibility.SwaggerCompatibility }, testConnector._apiDocument).ToArray();
ConnectorFunction performUnboundActionWithOrganization = functions.First(f => f.Name == "PerformUnboundActionWithOrganizationV2");

#pragma warning disable SA1118, SA1009, SA1111, SA1137

// response here is fully ignored
testConnector.SetResponseFromFile(@"Responses\Dataverse_Response_3.json");
_ = await performUnboundActionWithOrganization.InvokeAsync(
new FormulaValue[]
{
// required params
FormulaValue.New("https://aurorabapenv9984a.crm10.dynamics.com/"),
FormulaValue.New("someAction"),

// optional param "item" with (in swagger) "item-less array" (![])
FormulaValue.NewRecordFromFields(
new NamedValue(
"item",
new InMemoryTableValue(
IRContext.NotInSource(
RecordType.Empty()
.Add("arg1", FormulaType.String)
.Add("arg2", FormulaType.Decimal)
.Add("arg3", RecordType.Empty().Add("innerArg1", FormulaType.String))
.Add("arg4", RecordType.Empty().Add("innerArg2", FormulaType.String).ToTable())
.ToTable()
),
new DValue<RecordValue>[]
{
DValue<RecordValue>.Of(FormulaValue.NewRecordFromFields(
new NamedValue("arg1", FormulaValue.New("foo")),
new NamedValue("arg2", FormulaValue.New(17.88m)),
new NamedValue("arg3", FormulaValue.NewRecordFromFields(
new NamedValue("innerArg1", FormulaValue.New("bar")))),
new NamedValue(
"arg4",
new InMemoryTableValue(
IRContext.NotInSource(RecordType.Empty().Add("innerArg2", FormulaType.String).ToTable()),
new DValue<RecordValue>[]
{
DValue<RecordValue>.Of(FormulaValue.NewRecordFromFields(
new NamedValue("innerArg2", FormulaValue.New("xyz"))))
}
))))
}
)))
},
runtimeContext,
CancellationToken.None);

#pragma warning restore SA1118, SA1009, SA1111, SA1137

string actual = testConnector._log.ToString();
var version = PowerPlatformConnectorClient.Version;

// Validate that we send the 'array' as expected
string expected = @$"POST https://tip1002-002.azure-apihub.net/invoke
authority: tip1002-002.azure-apihub.net
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJ...
organization: https://aurorabapenv9984a.crm10.dynamics.com/
path: /invoke
scheme: https
x-ms-client-environment-id: /providers/Microsoft.PowerApps/environments/b29c41cf-173b-e469-830b-4f00163d296b
x-ms-client-session-id: a41bd03b-6c3c-4509-a844-e8c51b61f878
x-ms-request-method: POST
x-ms-request-url: /apim/commondataserviceforapps/82728ddb6bfa461ea3e50e17da8ab164/flow/api/data/v9.1.0/someAction/v2-fake
x-ms-user-agent: PowerFx/{version}
[content-header] Content-Type: application/json; charset=utf-8
[body] [{{""arg1"":""foo"",""arg2"":17.88,""arg3"":{{""innerArg1"":""bar""}},""arg4"":[{{""innerArg2"":""xyz""}}]}}]
";

Assert.Equal<object>(expected, actual);
}

// ConnectorCompatibility element will determine if an internal parameters will be suggested.
[Theory]
[InlineData(ConnectorCompatibility.Default, "Office365Users.SearchUserV2(", "SearchUserV2({ searchTerm:String,top:Decimal,isSearchTermRequired:Boolean,skipToken:String })")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4683,6 +4683,78 @@
}
}
},
"/{connectionId}/flow/api/data/v9.1.0/{actionName}/v2-fake": {
"post": {
"tags": [
"PerformUnboundAction-Fake",
"Runtime"
],
"summary": "Perform an unbound action (Preview)",
"description": "Run a global Dataverse action in a Power Platform environment, including custom actions.",
"operationId": "PerformUnboundActionWithOrganizationV2",
"consumes": [],
"produces": [
"application/json",
"text/json"
],
"parameters": [
{
"name": "connectionId",
"in": "path",
"required": true,
"type": "string",
"x-ms-visibility": "internal"
},
{
"name": "organization",
"in": "header",
"description": "Choose an environment",
"required": true,
"x-ms-summary": "Environment",
"x-ms-visibility": "important",
"type": "string"
},
{
"name": "actionName",
"in": "path",
"description": "Choose an action",
"required": true,
"x-ms-url-encoding": "double",
"x-ms-summary": "Action Name",
"type": "string"
},
{
"name": "item",
"in": "body",
"description": "Action parameters",
"required": false,
"schema": {
"type": "array",
"items": {
"x-ms-internal-dev-comment": "There is no description of the fields here!"
}
},
"x-ms-summary": "Action parameters"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object"
}
},
"default": {
"description": "Operation Failed."
}
},
"deprecated": false,
"x-ms-visibility": "important",
"externalDocs": {
"url": "https://docs.microsoft.com/connectors/commondataserviceforapps/#perform-an-unbound-action-(preview)"
}
}
},
"/{connectionId}/api/data/v9.1/{entityName}({recordId})/{actionName}": {
"post": {
"tags": [
Expand Down