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
10 changes: 10 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,15 @@ public string? PathPrefix
/// </summary>
public int ComposableFunctionsExpansionDepth { get; set; } = 1;

/// <summary>
/// Gets/sets a value indicating whether to use HTTP PUT method for update operations by default
/// instead of PATCH when no UpdateRestrictions annotation is present in the CSDL.
/// If false (default), PATCH will be used for updates.
/// If true, PUT will be used for updates.
/// This setting is ignored when UpdateRestrictions annotations are present in the CSDL.
/// </summary>
public bool UseHttpPutForUpdate { get; set; } = false;

/// <summary>
/// Gets/Sets a value indicating whether common OData number parameters ($top, $skip) should use int32 format instead of int64.
/// </summary>
Expand Down Expand Up @@ -403,6 +412,7 @@ internal OpenApiConvertSettings Clone()
EnableAliasForOperationSegments = this.EnableAliasForOperationSegments,
UseStringArrayForQueryOptionsSchema = this.UseStringArrayForQueryOptionsSchema,
ComposableFunctionsExpansionDepth = this.ComposableFunctionsExpansionDepth,
UseHttpPutForUpdate = this.UseHttpPutForUpdate,
UseInt32ForPaginationParameters = this.UseInt32ForPaginationParameters,
UseInt32ForCountResponses = this.UseInt32ForCountResponses
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,16 @@ public void AddUpdateOperation(OpenApiPathItem item)
}
else
{
AddOperation(item, HttpMethod.Patch);
// When no explicit update method is specified in UpdateRestrictions,
// use the UseHttpPutForUpdate setting to determine the default method
if (Context?.Settings?.UseHttpPutForUpdate == true)
{
AddOperation(item, HttpMethod.Put);
}
else
{
AddOperation(item, HttpMethod.Patch);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,16 @@ protected override void SetOperations(OpenApiPathItem item)
}
else
{
AddOperation(item, HttpMethod.Patch);
// When no explicit update method is specified in UpdateRestrictions,
// use the UseHttpPutForUpdate setting to determine the default method
if (Context?.Settings?.UseHttpPutForUpdate == true)
{
AddOperation(item, HttpMethod.Put);
}
else
{
AddOperation(item, HttpMethod.Patch);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@

if (NavigationProperty.TargetMultiplicity() == EdmMultiplicity.Many)
{
// TODO: $ref also supports Get ?

Check warning on line 201 in src/Microsoft.OpenApi.OData.Reader/PathItem/NavigationPropertyPathItemHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment.
if (LastSegmentIsKeySegment)
{
if ((navPropReadRestriction?.ReadByKeyRestrictions?.IsReadable ?? true) &&
Expand Down Expand Up @@ -279,7 +279,16 @@
}
else
{
AddOperation(item, HttpMethod.Patch);
// When no explicit update method is specified in UpdateRestrictions,
// use the UseHttpPutForUpdate setting to determine the default method
if (Context?.Settings?.UseHttpPutForUpdate == true)
{
AddOperation(item, HttpMethod.Put);
}
else
{
AddOperation(item, HttpMethod.Patch);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ Microsoft.OpenApi.OData.OpenApiConvertSettings.UseStringArrayForQueryOptionsSche
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseStringArrayForQueryOptionsSchema.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseSuccessStatusCodeRange.get -> bool
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseSuccessStatusCodeRange.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseHttpPutForUpdate.get -> bool
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseHttpPutForUpdate.set -> void
Microsoft.OpenApi.OData.OpenApiConvertSettings.VerifyEdmModel.get -> bool
Microsoft.OpenApi.OData.OpenApiConvertSettings.VerifyEdmModel.set -> void
Microsoft.OpenApi.OData.Vocabulary.Core.LinkRelKey
Expand Down
32 changes: 32 additions & 0 deletions src/OoasUtil/ComLineProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ public ComLineProcessor(string[] args)
/// </summary>
public bool? RequireDerivedTypesConstraint { get; private set; }

/// <summary>
/// Use HTTP PUT method for update operations by default instead of PATCH.
/// </summary>
public bool? UseHttpPutForUpdate { get; private set; }

/// <summary>
/// Process the arguments.
/// </summary>
Expand Down Expand Up @@ -227,6 +232,14 @@ public bool Process()
}
break;

case "--useputforupdate":
case "-put":
if (!ProcessUseHttpPutForUpdate(true))
{
return false;
}
break;

default:
PrintUsage();
return false;
Expand Down Expand Up @@ -285,6 +298,11 @@ public bool Process()
DisableSchemaExamples = false;
}

if (UseHttpPutForUpdate == null)
{
UseHttpPutForUpdate = false;
}

_continue = ValidateArguments();
return _continue;
}
Expand Down Expand Up @@ -419,6 +437,19 @@ private bool ProcessDisableSchemaExamples(bool disableSchemaExamples)
return true;
}

private bool ProcessUseHttpPutForUpdate(bool useHttpPutForUpdate)
{
if (UseHttpPutForUpdate != null)
{
Console.WriteLine("[Error:] Multiple [--useputforupdate|-put] are not allowed.\n");
PrintUsage();
return false;
}

UseHttpPutForUpdate = useHttpPutForUpdate;
return true;
}

private bool ProcessTarget(int version)
{
if (Version != null)
Expand Down Expand Up @@ -484,6 +515,7 @@ public static void PrintUsage()
sb.Append(" --enablepagination|-p\t\t\tSet the output to expose pagination for collections.\n");
sb.Append(" --enableunqualifiedcall|-u\t\t\tSet the output to use unqualified calls for bound operations.\n");
sb.Append(" --disableschemaexamples|-x\t\t\tDisable examples in the schema.\n");
sb.Append(" --useputforupdate|-put\t\t\tUse HTTP PUT method for update operations instead of PATCH by default.\n");
sb.Append(" --json|-j\t\t\tSet the output format as JSON.\n");
sb.Append(" --yaml|-y\t\t\tSet the output format as YAML.\n");
sb.Append(" --specversion|-s IntVersion\tSet the OpenApi Specification version of the output. Only 2 or 3 are supported.\n");
Expand Down
1 change: 1 addition & 0 deletions src/OoasUtil/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ static async System.Threading.Tasks.Task<int> Main(string[] args)
EnableUnqualifiedCall = processor.EnableUnqualifiedCall.Value,
ShowSchemaExamples = !processor.DisableSchemaExamples.Value,
OpenApiSpecVersion = processor.Version.Value,
UseHttpPutForUpdate = processor.UseHttpPutForUpdate.Value,
};

if (processor.IsLocalFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,101 @@ public void CreatesComplexPropertyPathsBasedOnTargetPathAnnotations(string reada
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
}
}

[Theory]
[InlineData(false, 2)]
[InlineData(true, 2)]
public void CreatesComplexPropertyPathItemUsesHttpPutForUpdateWhenSettingIsEnabled(bool useHttpPutForUpdate, int operationCount)
{
// Arrange
var annotation = @"
<Annotation Term=""Org.OData.Capabilities.V1.UpdateRestrictions"">
<Record>
<PropertyValue Property=""Updatable"" Bool=""true"" />
</Record>
</Annotation>
<Annotation Term=""Org.OData.Capabilities.V1.ReadRestrictions"">
<Record>
<PropertyValue Property=""Readable"" Bool=""true"" />
</Record>
</Annotation>";
var target = @"""NS.Customer/BillingAddress""";
var model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: annotation, target: target);
var convertSettings = new OpenApiConvertSettings
{
UseHttpPutForUpdate = useHttpPutForUpdate
};
var context = new ODataContext(model, convertSettings);
var entitySet = model.EntityContainer.FindEntitySet("Customers");
Assert.NotNull(entitySet); // guard
var entityType = entitySet.EntityType;
var property = entityType.FindProperty("BillingAddress");
Assert.NotNull(property); // guard
var path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entityType), new ODataComplexPropertySegment(property as IEdmStructuralProperty));
Assert.Equal(ODataPathKind.ComplexProperty, path.Kind); // guard

// Act
var pathItem = _pathItemHandler.CreatePathItem(context, path);

// Assert
Assert.NotNull(pathItem);
Assert.Equal(operationCount, pathItem.Operations?.Count ?? 0);

Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
if (useHttpPutForUpdate)
{
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Put));
Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Patch));
}
else
{
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Patch));
Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Put));
}
}

[Fact]
public void CreateComplexPropertyPathItemPrefersUpdateMethodAnnotationOverUseHttpPutForUpdateSetting()
{
// Arrange - annotation specifies PUT explicitly, setting is disabled (default PATCH)
var annotation = @"
<Annotation Term=""Org.OData.Capabilities.V1.UpdateRestrictions"">
<Record>
<PropertyValue Property=""UpdateMethod"">
<EnumMember>Org.OData.Capabilities.V1.HttpMethod/PUT</EnumMember>
</PropertyValue>
<PropertyValue Property=""Updatable"" Bool=""true"" />
</Record>
</Annotation>
<Annotation Term=""Org.OData.Capabilities.V1.ReadRestrictions"">
<Record>
<PropertyValue Property=""Readable"" Bool=""true"" />
</Record>
</Annotation>";
var target = @"""NS.Customer/BillingAddress""";
var model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: annotation, target: target);
var convertSettings = new OpenApiConvertSettings
{
UseHttpPutForUpdate = false // Setting says use PATCH (default)
};
var context = new ODataContext(model, convertSettings);
var entitySet = model.EntityContainer.FindEntitySet("Customers");
Assert.NotNull(entitySet); // guard
var entityType = entitySet.EntityType;
var property = entityType.FindProperty("BillingAddress");
Assert.NotNull(property); // guard
var path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entityType), new ODataComplexPropertySegment(property as IEdmStructuralProperty));
Assert.Equal(ODataPathKind.ComplexProperty, path.Kind); // guard

// Act
var pathItem = _pathItemHandler.CreatePathItem(context, path);

// Assert
Assert.NotNull(pathItem);
Assert.Equal(2, pathItem.Operations?.Count ?? 0);
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
// Should use PUT from annotation, not PATCH from setting
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Put));
Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Patch));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,67 @@
VerifyPathItemOperations(annotation, expected);
}

[Theory]
[InlineData(false, new string[] { "get", "patch", "delete" })]
[InlineData(true, new string[] { "get", "put", "delete" })]
public void CreateEntityPathItemUsesHttpPutForUpdateWhenSettingIsEnabled(bool useHttpPutForUpdate, string[] expected)
{
// Arrange
IEdmModel model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: "");
OpenApiConvertSettings settings = new OpenApiConvertSettings
{
UseHttpPutForUpdate = useHttpPutForUpdate
};
ODataContext context = new ODataContext(model, settings);
IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
Assert.NotNull(entitySet); // guard
ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entitySet.EntityType));

// Act
var pathItem = _pathItemHandler.CreatePathItem(context, path);

// Assert
Assert.NotNull(pathItem);

Assert.NotNull(pathItem.Operations);
Assert.NotEmpty(pathItem.Operations);
Assert.Equal(expected, pathItem.Operations.Select(e => e.Key.ToString().ToLowerInvariant()));
}

[Fact]
public void CreateEntityPathItemPrefersUpdateMethodAnnotationOverUseHttpPutForUpdateSetting()
{
// Arrange - annotation specifies PUT explicitly, setting is disabled (default PATCH)
string annotation = @"
<Annotation Term=""Org.OData.Capabilities.V1.UpdateRestrictions"">
<Record>
<PropertyValue Property=""UpdateMethod"">
<EnumMember>Org.OData.Capabilities.V1.HttpMethod/PUT</EnumMember>
</PropertyValue>
</Record>
</Annotation>";

IEdmModel model = EntitySetPathItemHandlerTests.GetEdmModel(annotation);
OpenApiConvertSettings settings = new OpenApiConvertSettings
{
UseHttpPutForUpdate = false // Setting says use PATCH (default)
};
ODataContext context = new ODataContext(model, settings);
IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
Assert.NotNull(entitySet); // guard
ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entitySet.EntityType));

// Act
var pathItem = _pathItemHandler.CreatePathItem(context, path);

// Assert
Assert.NotNull(pathItem);
Assert.NotNull(pathItem.Operations);
Assert.NotEmpty(pathItem.Operations);
// Should use PUT from annotation, not PATCH from setting
Assert.Equal(new string[] { "get", "put", "delete" }, pathItem.Operations.Select(e => e.Key.ToString().ToLowerInvariant()));

Check warning on line 293 in test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/EntityPathItemHandlerTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET.OData&issues=AZ0HzSc71jy-UnSm9r9Z&open=AZ0HzSc71jy-UnSm9r9Z&pullRequest=798
}

private void VerifyPathItemOperations(string annotation, string[] expected)
{
// Arrange
Expand Down
Loading
Loading