Skip to content
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
12 changes: 11 additions & 1 deletion src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/// <summary>
/// Gets/sets the service root.
/// </summary>
public Uri ServiceRoot { get; set; } = new Uri("http://localhost");

Check warning on line 22 in src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs. (https://rules.sonarsource.com/csharp/RSPEC-1075)

/// <summary>
/// Get/set the metadata version.
Expand Down Expand Up @@ -337,6 +337,15 @@
/// </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;

internal OpenApiConvertSettings Clone()
{
var newSettings = new OpenApiConvertSettings
Expand Down Expand Up @@ -392,7 +401,8 @@
SemVerVersion = this.SemVerVersion,
EnableAliasForOperationSegments = this.EnableAliasForOperationSegments,
UseStringArrayForQueryOptionsSchema = this.UseStringArrayForQueryOptionsSchema,
ComposableFunctionsExpansionDepth = this.ComposableFunctionsExpansionDepth
ComposableFunctionsExpansionDepth = this.ComposableFunctionsExpansionDepth,
UseHttpPutForUpdate = this.UseHttpPutForUpdate
};

return newSettings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,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)

Check warning on line 104 in src/Microsoft.OpenApi.OData.Reader/PathItem/ComplexPropertyItemHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this unnecessary check for null. (https://rules.sonarsource.com/csharp/RSPEC-2589)
{
AddOperation(item, HttpMethod.Put);
}
else
{
AddOperation(item, HttpMethod.Patch);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
protected override ODataPathKind HandleKind => ODataPathKind.Entity;

/// <inheritdoc/>
protected override void SetOperations(OpenApiPathItem item)

Check warning on line 31 in src/Microsoft.OpenApi.OData.Reader/PathItem/EntityPathItemHandler.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
var readRestrictions = string.IsNullOrEmpty(TargetPath) ? null : Context?.Model.GetRecord<ReadRestrictionsType>(TargetPath, CapabilitiesConstants.ReadRestrictions);

Expand Down Expand Up @@ -67,7 +67,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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
private IEdmEntityType? _navPropEntityType;

/// <inheritdoc/>
protected override void SetOperations(OpenApiPathItem item)

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

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 57 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
IEdmVocabularyAnnotatable? target = NavigationSource switch {
IEdmEntitySet es => es,
Expand Down Expand Up @@ -175,7 +175,7 @@
AddDeleteOperation(item, restriction);
}

private void AddGetOperation(OpenApiPathItem item, NavigationPropertyRestriction? restriction)

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

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
var navPropReadRestriction = string.IsNullOrEmpty(TargetPath) ? null : Context?.Model.GetRecord<ReadRestrictionsType>(TargetPath, CapabilitiesConstants.ReadRestrictions);
navPropReadRestriction?.MergePropertiesIfNull(restriction?.ReadRestrictions);
Expand All @@ -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. (https://rules.sonarsource.com/csharp/RSPEC-1135)
if (LastSegmentIsKeySegment)
{
if ((navPropReadRestriction?.ReadByKeyRestrictions?.IsReadable ?? true) &&
Expand Down Expand Up @@ -255,7 +255,7 @@
AddOperation(item, HttpMethod.Delete);
}
else if (navPropDeleteRestriction?.Deletable ?? false)
{

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

View workflow job for this annotation

GitHub Actions / Build

Either merge this branch with the identical one on line 254 or change one of the implementations. (https://rules.sonarsource.com/csharp/RSPEC-1871)
// Add delete operation for non-contained nav. props only if explicitly set to true via annotation
// Note: Use Deletable and NOT IsDeletable
AddOperation(item, HttpMethod.Delete);
Expand All @@ -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 All @@ -300,7 +309,7 @@
}

/// <inheritdoc/>
protected override void SetExtensions(OpenApiPathItem item)

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

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (Context is null || !Context.Settings.ShowMsDosGroupPath)
{
Expand All @@ -324,7 +333,7 @@

var npSegment = path.LastSegment is ODataNavigationPropertySegment npS ?
npS :
path.Segments[path.Count - 2] is ODataNavigationPropertySegment npSegmentFallback ?

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

View workflow job for this annotation

GitHub Actions / Build

Extract this nested ternary operation into an independent statement. (https://rules.sonarsource.com/csharp/RSPEC-3358)
npSegmentFallback :
null;
if (NavigationProperty != npSegment?.NavigationProperty)
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 @@ -221,6 +221,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 @@ -14,7 +14,7 @@
{
static async System.Threading.Tasks.Task<int> Main(string[] args)
{
// args = new[] { "--json", "--input", @"E:\work\OneApiDesign\test\TripService.OData.xml", "-o", @"E:\work\OneApiDesign\test1\Trip.json" };

Check warning on line 17 in src/OoasUtil/Program.cs

View workflow job for this annotation

GitHub Actions / Build

Remove this commented out code. (https://rules.sonarsource.com/csharp/RSPEC-125)
// args = new[] { "--yaml", "-i", @"E:\work\OneApiDesign\test\TripService.OData.xml", "-o", @"E:\work\OneApiDesign\test1\Trip.yaml" };
// args = new[] { "--yaml", "--input", @"http://services.odata.org/TrippinRESTierService", "-o", @"E:\work\OneApiDesign\test1\TripUrl.yaml" };
// args = new[] { "--json", "-i", @"http://services.odata.org/TrippinRESTierService", "-o", @"E:\work\OneApiDesign\test1\TripUrl.json" };
Expand Down Expand Up @@ -42,6 +42,7 @@
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 @@ public void CreateEntityPathItemWorksForUpdateMethodRestrictionsCapabilities(boo
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()));
}

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