Skip to content

Commit bef4c96

Browse files
authored
feat: adds settings to map the update verb to put
Merge pull request #770 from microsoft/feat/use-put-for-update
2 parents 305eacc + 2da64a7 commit bef4c96

File tree

10 files changed

+355
-3
lines changed

10 files changed

+355
-3
lines changed

src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,15 @@ public string? PathPrefix
337337
/// </summary>
338338
public int ComposableFunctionsExpansionDepth { get; set; } = 1;
339339

340+
/// <summary>
341+
/// Gets/sets a value indicating whether to use HTTP PUT method for update operations by default
342+
/// instead of PATCH when no UpdateRestrictions annotation is present in the CSDL.
343+
/// If false (default), PATCH will be used for updates.
344+
/// If true, PUT will be used for updates.
345+
/// This setting is ignored when UpdateRestrictions annotations are present in the CSDL.
346+
/// </summary>
347+
public bool UseHttpPutForUpdate { get; set; } = false;
348+
340349
/// <summary>
341350
/// Gets/Sets a value indicating whether common OData number parameters ($top, $skip) should use int32 format instead of int64.
342351
/// </summary>
@@ -403,6 +412,7 @@ internal OpenApiConvertSettings Clone()
403412
EnableAliasForOperationSegments = this.EnableAliasForOperationSegments,
404413
UseStringArrayForQueryOptionsSchema = this.UseStringArrayForQueryOptionsSchema,
405414
ComposableFunctionsExpansionDepth = this.ComposableFunctionsExpansionDepth,
415+
UseHttpPutForUpdate = this.UseHttpPutForUpdate,
406416
UseInt32ForPaginationParameters = this.UseInt32ForPaginationParameters,
407417
UseInt32ForCountResponses = this.UseInt32ForCountResponses
408418
};

src/Microsoft.OpenApi.OData.Reader/PathItem/ComplexPropertyItemHandler.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,16 @@ public void AddUpdateOperation(OpenApiPathItem item)
9999
}
100100
else
101101
{
102-
AddOperation(item, HttpMethod.Patch);
102+
// When no explicit update method is specified in UpdateRestrictions,
103+
// use the UseHttpPutForUpdate setting to determine the default method
104+
if (Context?.Settings?.UseHttpPutForUpdate == true)
105+
{
106+
AddOperation(item, HttpMethod.Put);
107+
}
108+
else
109+
{
110+
AddOperation(item, HttpMethod.Patch);
111+
}
103112
}
104113
}
105114
}

src/Microsoft.OpenApi.OData.Reader/PathItem/EntityPathItemHandler.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,16 @@ protected override void SetOperations(OpenApiPathItem item)
6767
}
6868
else
6969
{
70-
AddOperation(item, HttpMethod.Patch);
70+
// When no explicit update method is specified in UpdateRestrictions,
71+
// use the UseHttpPutForUpdate setting to determine the default method
72+
if (Context?.Settings?.UseHttpPutForUpdate == true)
73+
{
74+
AddOperation(item, HttpMethod.Put);
75+
}
76+
else
77+
{
78+
AddOperation(item, HttpMethod.Patch);
79+
}
7180
}
7281
}
7382

src/Microsoft.OpenApi.OData.Reader/PathItem/NavigationPropertyPathItemHandler.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,16 @@ private void AddUpdateOperation(OpenApiPathItem item, UpdateRestrictionsType? up
279279
}
280280
else
281281
{
282-
AddOperation(item, HttpMethod.Patch);
282+
// When no explicit update method is specified in UpdateRestrictions,
283+
// use the UseHttpPutForUpdate setting to determine the default method
284+
if (Context?.Settings?.UseHttpPutForUpdate == true)
285+
{
286+
AddOperation(item, HttpMethod.Put);
287+
}
288+
else
289+
{
290+
AddOperation(item, HttpMethod.Patch);
291+
}
283292
}
284293
}
285294

src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ Microsoft.OpenApi.OData.OpenApiConvertSettings.UseStringArrayForQueryOptionsSche
225225
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseStringArrayForQueryOptionsSchema.set -> void
226226
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseSuccessStatusCodeRange.get -> bool
227227
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseSuccessStatusCodeRange.set -> void
228+
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseHttpPutForUpdate.get -> bool
229+
Microsoft.OpenApi.OData.OpenApiConvertSettings.UseHttpPutForUpdate.set -> void
228230
Microsoft.OpenApi.OData.OpenApiConvertSettings.VerifyEdmModel.get -> bool
229231
Microsoft.OpenApi.OData.OpenApiConvertSettings.VerifyEdmModel.set -> void
230232
Microsoft.OpenApi.OData.Vocabulary.Core.LinkRelKey

src/OoasUtil/ComLineProcessor.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ public ComLineProcessor(string[] args)
9999
/// </summary>
100100
public bool? RequireDerivedTypesConstraint { get; private set; }
101101

102+
/// <summary>
103+
/// Use HTTP PUT method for update operations by default instead of PATCH.
104+
/// </summary>
105+
public bool? UseHttpPutForUpdate { get; private set; }
106+
102107
/// <summary>
103108
/// Process the arguments.
104109
/// </summary>
@@ -227,6 +232,14 @@ public bool Process()
227232
}
228233
break;
229234

235+
case "--useputforupdate":
236+
case "-put":
237+
if (!ProcessUseHttpPutForUpdate(true))
238+
{
239+
return false;
240+
}
241+
break;
242+
230243
default:
231244
PrintUsage();
232245
return false;
@@ -285,6 +298,11 @@ public bool Process()
285298
DisableSchemaExamples = false;
286299
}
287300

301+
if (UseHttpPutForUpdate == null)
302+
{
303+
UseHttpPutForUpdate = false;
304+
}
305+
288306
_continue = ValidateArguments();
289307
return _continue;
290308
}
@@ -419,6 +437,19 @@ private bool ProcessDisableSchemaExamples(bool disableSchemaExamples)
419437
return true;
420438
}
421439

440+
private bool ProcessUseHttpPutForUpdate(bool useHttpPutForUpdate)
441+
{
442+
if (UseHttpPutForUpdate != null)
443+
{
444+
Console.WriteLine("[Error:] Multiple [--useputforupdate|-put] are not allowed.\n");
445+
PrintUsage();
446+
return false;
447+
}
448+
449+
UseHttpPutForUpdate = useHttpPutForUpdate;
450+
return true;
451+
}
452+
422453
private bool ProcessTarget(int version)
423454
{
424455
if (Version != null)
@@ -484,6 +515,7 @@ public static void PrintUsage()
484515
sb.Append(" --enablepagination|-p\t\t\tSet the output to expose pagination for collections.\n");
485516
sb.Append(" --enableunqualifiedcall|-u\t\t\tSet the output to use unqualified calls for bound operations.\n");
486517
sb.Append(" --disableschemaexamples|-x\t\t\tDisable examples in the schema.\n");
518+
sb.Append(" --useputforupdate|-put\t\t\tUse HTTP PUT method for update operations instead of PATCH by default.\n");
487519
sb.Append(" --json|-j\t\t\tSet the output format as JSON.\n");
488520
sb.Append(" --yaml|-y\t\t\tSet the output format as YAML.\n");
489521
sb.Append(" --specversion|-s IntVersion\tSet the OpenApi Specification version of the output. Only 2 or 3 are supported.\n");

src/OoasUtil/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ static async System.Threading.Tasks.Task<int> Main(string[] args)
4242
EnableUnqualifiedCall = processor.EnableUnqualifiedCall.Value,
4343
ShowSchemaExamples = !processor.DisableSchemaExamples.Value,
4444
OpenApiSpecVersion = processor.Version.Value,
45+
UseHttpPutForUpdate = processor.UseHttpPutForUpdate.Value,
4546
};
4647

4748
if (processor.IsLocalFile)

test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/ComplexPropertyPathItemHandlerTests.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,101 @@ public void CreatesComplexPropertyPathsBasedOnTargetPathAnnotations(string reada
263263
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
264264
}
265265
}
266+
267+
[Theory]
268+
[InlineData(false, 2)]
269+
[InlineData(true, 2)]
270+
public void CreatesComplexPropertyPathItemUsesHttpPutForUpdateWhenSettingIsEnabled(bool useHttpPutForUpdate, int operationCount)
271+
{
272+
// Arrange
273+
var annotation = @"
274+
<Annotation Term=""Org.OData.Capabilities.V1.UpdateRestrictions"">
275+
<Record>
276+
<PropertyValue Property=""Updatable"" Bool=""true"" />
277+
</Record>
278+
</Annotation>
279+
<Annotation Term=""Org.OData.Capabilities.V1.ReadRestrictions"">
280+
<Record>
281+
<PropertyValue Property=""Readable"" Bool=""true"" />
282+
</Record>
283+
</Annotation>";
284+
var target = @"""NS.Customer/BillingAddress""";
285+
var model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: annotation, target: target);
286+
var convertSettings = new OpenApiConvertSettings
287+
{
288+
UseHttpPutForUpdate = useHttpPutForUpdate
289+
};
290+
var context = new ODataContext(model, convertSettings);
291+
var entitySet = model.EntityContainer.FindEntitySet("Customers");
292+
Assert.NotNull(entitySet); // guard
293+
var entityType = entitySet.EntityType;
294+
var property = entityType.FindProperty("BillingAddress");
295+
Assert.NotNull(property); // guard
296+
var path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entityType), new ODataComplexPropertySegment(property as IEdmStructuralProperty));
297+
Assert.Equal(ODataPathKind.ComplexProperty, path.Kind); // guard
298+
299+
// Act
300+
var pathItem = _pathItemHandler.CreatePathItem(context, path);
301+
302+
// Assert
303+
Assert.NotNull(pathItem);
304+
Assert.Equal(operationCount, pathItem.Operations?.Count ?? 0);
305+
306+
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
307+
if (useHttpPutForUpdate)
308+
{
309+
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Put));
310+
Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Patch));
311+
}
312+
else
313+
{
314+
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Patch));
315+
Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Put));
316+
}
317+
}
318+
319+
[Fact]
320+
public void CreateComplexPropertyPathItemPrefersUpdateMethodAnnotationOverUseHttpPutForUpdateSetting()
321+
{
322+
// Arrange - annotation specifies PUT explicitly, setting is disabled (default PATCH)
323+
var annotation = @"
324+
<Annotation Term=""Org.OData.Capabilities.V1.UpdateRestrictions"">
325+
<Record>
326+
<PropertyValue Property=""UpdateMethod"">
327+
<EnumMember>Org.OData.Capabilities.V1.HttpMethod/PUT</EnumMember>
328+
</PropertyValue>
329+
<PropertyValue Property=""Updatable"" Bool=""true"" />
330+
</Record>
331+
</Annotation>
332+
<Annotation Term=""Org.OData.Capabilities.V1.ReadRestrictions"">
333+
<Record>
334+
<PropertyValue Property=""Readable"" Bool=""true"" />
335+
</Record>
336+
</Annotation>";
337+
var target = @"""NS.Customer/BillingAddress""";
338+
var model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: annotation, target: target);
339+
var convertSettings = new OpenApiConvertSettings
340+
{
341+
UseHttpPutForUpdate = false // Setting says use PATCH (default)
342+
};
343+
var context = new ODataContext(model, convertSettings);
344+
var entitySet = model.EntityContainer.FindEntitySet("Customers");
345+
Assert.NotNull(entitySet); // guard
346+
var entityType = entitySet.EntityType;
347+
var property = entityType.FindProperty("BillingAddress");
348+
Assert.NotNull(property); // guard
349+
var path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entityType), new ODataComplexPropertySegment(property as IEdmStructuralProperty));
350+
Assert.Equal(ODataPathKind.ComplexProperty, path.Kind); // guard
351+
352+
// Act
353+
var pathItem = _pathItemHandler.CreatePathItem(context, path);
354+
355+
// Assert
356+
Assert.NotNull(pathItem);
357+
Assert.Equal(2, pathItem.Operations?.Count ?? 0);
358+
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Get));
359+
// Should use PUT from annotation, not PATCH from setting
360+
Assert.True(pathItem.Operations.ContainsKey(HttpMethod.Put));
361+
Assert.False(pathItem.Operations.ContainsKey(HttpMethod.Patch));
362+
}
266363
}

test/Microsoft.OpenAPI.OData.Reader.Tests/PathItem/EntityPathItemHandlerTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,67 @@ public void CreateEntityPathItemWorksForUpdateMethodRestrictionsCapabilities(boo
232232
VerifyPathItemOperations(annotation, expected);
233233
}
234234

235+
[Theory]
236+
[InlineData(false, new string[] { "get", "patch", "delete" })]
237+
[InlineData(true, new string[] { "get", "put", "delete" })]
238+
public void CreateEntityPathItemUsesHttpPutForUpdateWhenSettingIsEnabled(bool useHttpPutForUpdate, string[] expected)
239+
{
240+
// Arrange
241+
IEdmModel model = EntitySetPathItemHandlerTests.GetEdmModel(annotation: "");
242+
OpenApiConvertSettings settings = new OpenApiConvertSettings
243+
{
244+
UseHttpPutForUpdate = useHttpPutForUpdate
245+
};
246+
ODataContext context = new ODataContext(model, settings);
247+
IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
248+
Assert.NotNull(entitySet); // guard
249+
ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entitySet.EntityType));
250+
251+
// Act
252+
var pathItem = _pathItemHandler.CreatePathItem(context, path);
253+
254+
// Assert
255+
Assert.NotNull(pathItem);
256+
257+
Assert.NotNull(pathItem.Operations);
258+
Assert.NotEmpty(pathItem.Operations);
259+
Assert.Equal(expected, pathItem.Operations.Select(e => e.Key.ToString().ToLowerInvariant()));
260+
}
261+
262+
[Fact]
263+
public void CreateEntityPathItemPrefersUpdateMethodAnnotationOverUseHttpPutForUpdateSetting()
264+
{
265+
// Arrange - annotation specifies PUT explicitly, setting is disabled (default PATCH)
266+
string annotation = @"
267+
<Annotation Term=""Org.OData.Capabilities.V1.UpdateRestrictions"">
268+
<Record>
269+
<PropertyValue Property=""UpdateMethod"">
270+
<EnumMember>Org.OData.Capabilities.V1.HttpMethod/PUT</EnumMember>
271+
</PropertyValue>
272+
</Record>
273+
</Annotation>";
274+
275+
IEdmModel model = EntitySetPathItemHandlerTests.GetEdmModel(annotation);
276+
OpenApiConvertSettings settings = new OpenApiConvertSettings
277+
{
278+
UseHttpPutForUpdate = false // Setting says use PATCH (default)
279+
};
280+
ODataContext context = new ODataContext(model, settings);
281+
IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("Customers");
282+
Assert.NotNull(entitySet); // guard
283+
ODataPath path = new ODataPath(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entitySet.EntityType));
284+
285+
// Act
286+
var pathItem = _pathItemHandler.CreatePathItem(context, path);
287+
288+
// Assert
289+
Assert.NotNull(pathItem);
290+
Assert.NotNull(pathItem.Operations);
291+
Assert.NotEmpty(pathItem.Operations);
292+
// Should use PUT from annotation, not PATCH from setting
293+
Assert.Equal(new string[] { "get", "put", "delete" }, pathItem.Operations.Select(e => e.Key.ToString().ToLowerInvariant()));
294+
}
295+
235296
private void VerifyPathItemOperations(string annotation, string[] expected)
236297
{
237298
// Arrange

0 commit comments

Comments
 (0)