Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
13 changes: 8 additions & 5 deletions src/Core/Services/OpenAPI/OpenApiDocumentor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ private static void AddCustomHeadersToOperation(OpenApiOperation operation)

/// <summary>
/// This method adds the input parameters from the stored procedure definition to the OpenApi operation parameters.
/// A input parameter will be marked REQUIRED if default value is not available.
/// A input parameter will be marked REQUIRED when it is explicitly flagged in config or when no default value is available.
/// </summary>
private static void AddStoredProcedureInputParameters(OpenApiOperation operation, StoredProcedureDefinition spDefinition)
{
Expand All @@ -690,7 +690,7 @@ private static void AddStoredProcedureInputParameters(OpenApiOperation operation
GetOpenApiQueryParameter(
name: paramKey,
description: "Input parameter for stored procedure arguments",
required: false,
required: parameterDefinition.Required ?? !parameterDefinition.HasConfigDefault,
type: TypeHelper.GetJsonDataTypeFromSystemType(parameterDefinition.SystemType).ToString().ToLower()
)
);
Expand Down Expand Up @@ -1135,9 +1135,9 @@ private static bool IsRequestBodyRequired(SourceDefinition sourceDef, bool consi
StoredProcedureDefinition spDef = (StoredProcedureDefinition)sourceDef;
foreach (KeyValuePair<string, ParameterDefinition> parameterMetadata in spDef.Parameters)
{
// A parameter which does not have any of the following properties
// A parameter which is explicitly marked required or has no default value
// results in the body being required so that a value can be provided.
if (!parameterMetadata.Value.HasConfigDefault)
if (parameterMetadata.Value.Required ?? !parameterMetadata.Value.HasConfigDefault)
{
requestBodyRequired = true;
break;
Expand Down Expand Up @@ -1422,7 +1422,10 @@ private static OpenApiSchema CreateSpRequestComponentSchema(Dictionary<string, P
Default = def.Default is not null ? new OpenApiString(def.Default) : null
});

if (def.Required == true)
// A parameter is required when it is explicitly marked required in the runtime config,
// or when no default value is available, in which case a value must be provided in the
// request body. This mirrors the logic used by IsRequestBodyRequired.
if (def.Required ?? !def.HasConfigDefault)
{
Comment thread
souvikghosh04 marked this conversation as resolved.
required.Add(parameter);
}
Expand Down
120 changes: 117 additions & 3 deletions src/Service.Tests/OpenApiDocumentor/StoredProcedureGeneration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,72 @@ public static void CreateEntities()
Relationships: null,
Description: "Represents a stored procedure for books");

// Entity whose "title" parameter has a config-provided default value.
// Because a default is available, "title" must not be flagged as required, while
// "publisher_id" (no default) must remain required.
Entity entity2 = new(
Source: new(
Object: "insert_book",
EntitySourceType.StoredProcedure,
Parameters: new List<ParameterMetadata>
{
new() { Name = "title", Default = "Sample Title" }
},
KeyFields: null),
Fields: null,
GraphQL: new(Singular: null, Plural: null, Enabled: false),
Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
Mappings: null,
Relationships: null,
Description: "Stored procedure with a parameter default");

// Entity whose "title" parameter is explicitly marked required: false in config.
// Even though it has no default value, the explicit override must be honored so that only
// "id" (no default, no override) remains required.
Entity entity3 = new(
Source: new(
Object: "update_book_title",
EntitySourceType.StoredProcedure,
Parameters: new List<ParameterMetadata>
{
new() { Name = "title", Required = false }
},
KeyFields: null),
Fields: null,
GraphQL: new(Singular: null, Plural: null, Enabled: false),
Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
Mappings: null,
Relationships: null,
Description: "Stored procedure with an explicit required override");

// Entity whose only parameter is explicitly marked required: false in config.
// Because no parameter is required, requestBody.Required and the GET query parameter's
// required flag must both be false, and the schema-level required set must be empty.
Entity entity4 = new(
Source: new(
Object: "get_publisher_by_id",
EntitySourceType.StoredProcedure,
Parameters: new List<ParameterMetadata>
{
new() { Name = "id", Required = false }
},
KeyFields: null),
Fields: null,
GraphQL: new(Singular: null, Plural: null, Enabled: false),
Rest: new(Methods: EntityRestOptions.DEFAULT_SUPPORTED_VERBS),
Permissions: OpenApiTestBootstrap.CreateBasicPermissions(),
Mappings: null,
Relationships: null,
Description: "Stored procedure with all parameters marked required: false");

Dictionary<string, Entity> entities = new()
{
{ "sp1", entity1 }
{ "sp1", entity1 },
{ "sp2", entity2 },
{ "sp3", entity3 },
{ "sp4", entity4 }
};

_runtimeEntities = new(entities);
Expand All @@ -74,13 +137,23 @@ public static void CreateEntities()
/// <summary>
/// Validates that the generated request body references stored procedure parameters
/// and not result set columns.
/// Also validates that the request body schema component flags the expected parameters
/// as required: a parameter is required when it has no default value and is not explicitly
/// marked required: false in the runtime config.
/// Also validates the body-level required flag on the OpenApiRequestBody object, which
/// should be true when any parameter is required and false when all parameters are optional.
/// </summary>
/// <param name="entityName">Entity name</param>
/// <param name="expectedParameters">Expected parameters in request body</param>
/// <param name="expectedParametersJsonTypes">Expected parameter value types in request body.</param>
[DataRow("sp1", new string[] { "title", "publisher_name" }, new string[] { "string", "string" }, DisplayName = "Validate request body parameters and parameter Json data types.")]
/// <param name="expectedRequiredParameters">Expected parameters flagged as required in the schema component.</param>
/// <param name="expectedRequestBodyRequired">Expected value of the body-level required flag.</param>
[DataRow("sp1", new string[] { "title", "publisher_name" }, new string[] { "string", "string" }, new string[] { "title", "publisher_name" }, true, DisplayName = "Parameters without defaults are all required.")]
[DataRow("sp2", new string[] { "title", "publisher_id" }, new string[] { "string", "integer" }, new string[] { "publisher_id" }, true, DisplayName = "Parameter with a config default is not required.")]
[DataRow("sp3", new string[] { "id", "title" }, new string[] { "integer", "string" }, new string[] { "id" }, true, DisplayName = "Parameter explicitly marked required: false is not required.")]
[DataRow("sp4", new string[] { "id" }, new string[] { "integer" }, new string[] { }, false, DisplayName = "All parameters marked required: false produces empty required set and optional request body.")]
[DataTestMethod]
public void ValidateRequestBodyContents(string entityName, string[] expectedParameters, string[] expectedParametersJsonTypes)
public void ValidateRequestBodyContents(string entityName, string[] expectedParameters, string[] expectedParametersJsonTypes, string[] expectedRequiredParameters, bool expectedRequestBodyRequired)
{
Dictionary<OperationType, bool> configuredOperations = ResolveConfiguredOperations(_runtimeEntities[entityName]);
foreach (OperationType opType in configuredOperations.Keys)
Expand All @@ -100,7 +173,17 @@ public void ValidateRequestBodyContents(string entityName, string[] expectedPara
OpenApiReference schemaComponentReference = GetRequestBodyReference(requestBody);
string expectedSchemaReferenceId = $"{entityName}{OpenApiDocumentor.SP_REQUEST_SUFFIX}";

// Validate the body-level required flag.
Assert.AreEqual(expectedRequestBodyRequired, requestBody.Required, message: "Unexpected request body required value.");

ValidateOpenApiReferenceContents(schemaComponentReference, expectedSchemaReferenceId, expectedParameters, expectedParametersJsonTypes);

// Validate that the expected parameters are marked as required in the schema component.
ISet<string> requiredParameters = _openApiDocument.Components.Schemas[expectedSchemaReferenceId].Required ?? new HashSet<string>();
CollectionAssert.AreEquivalent(
expectedRequiredParameters,
requiredParameters.ToArray(),
message: "Unexpected required parameters in request body schema component.");
}
Comment thread
souvikghosh04 marked this conversation as resolved.
}

Expand Down Expand Up @@ -149,6 +232,37 @@ public void OpenApiDocumentor_TagsIncludeEntityDescription()
$"Expected tag for '{entityName}' with description '{expectedDescription}' not found.");
}

/// <summary>
/// Validates that the generated GET operation query parameters for stored procedure entities
/// have the correct required flag: a parameter without a config default and not explicitly
/// marked required: false is required; a parameter explicitly marked required: false is not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Change false is not to true

/// </summary>
/// <param name="entityName">Entity name.</param>
/// <param name="expectedParameters">Expected query parameter names.</param>
/// <param name="expectedRequired">Whether each corresponding query parameter is expected to be required.</param>
[DataRow("sp1", new string[] { "title", "publisher_name" }, new bool[] { true, true }, DisplayName = "GET parameters without defaults are required.")]
[DataRow("sp4", new string[] { "id" }, new bool[] { false }, DisplayName = "GET parameter explicitly marked required: false is not required.")]
[DataTestMethod]
public void ValidateGetQueryParameters(string entityName, string[] expectedParameters, bool[] expectedRequired)
{
OpenApiOperation getOperation = _openApiDocument.Paths["/" + entityName].Operations[OperationType.Get];
Assert.IsNotNull(getOperation, "GET operation not found.");

// Filter to query parameters only (excludes the Authorization and X-MS-API-ROLE header parameters).
List<OpenApiParameter> spParams = getOperation.Parameters
.Where(p => p.In == ParameterLocation.Query)
.ToList();

Assert.AreEqual(expectedParameters.Length, spParams.Count, "Unexpected number of GET query parameters.");

for (int i = 0; i < expectedParameters.Length; i++)
{
OpenApiParameter param = spParams.FirstOrDefault(p => p.Name == expectedParameters[i]);
Assert.IsNotNull(param, $"Parameter '{expectedParameters[i]}' not found in GET query parameters.");
Assert.AreEqual(expectedRequired[i], param.Required, $"Unexpected required value for GET query parameter '{expectedParameters[i]}'.");
}
}

/// <summary>
/// Validates that the provided OpenApiReference object has the expected schema reference id
/// and that that id is present in the list of component schema in the OpenApi document.
Expand Down