public static bool ValidateDocument( ILogger logger, OpenApiDocument apiDocument, ApiOptionsValidation validationOptions) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(apiDocument); ArgumentNullException.ThrowIfNull(validationOptions); var logItems = new List <LogKeyValueItem>(); logItems.AddRange(ValidateServers(validationOptions, apiDocument.Servers)); logItems.AddRange(ValidateSchemas(validationOptions, apiDocument.Components.Schemas.Values)); logItems.AddRange(ValidateOperations(validationOptions, apiDocument.Paths, apiDocument.Components.Schemas)); logItems.AddRange(ValidatePathsAndOperations(validationOptions, apiDocument.Paths)); logItems.AddRange(ValidateOperationsParametersAndResponses(validationOptions, apiDocument.Paths.Values)); foreach (var item in logItems) { item.Key = item.Key.Insert(0, " "); } logger.LogKeyValueItems(logItems); return(logItems.All(x => x.LogCategory != LogCategoryType.Error)); }
/// <summary> /// Check for response types according to operation/global parameters. /// </summary> /// <param name="validationOptions">The validation options.</param> /// <param name="path">The path.</param> /// <returns>List of possible validation errors</returns> public static List <LogKeyValueItem> ValidateGetOperations( ApiOptionsValidation validationOptions, KeyValuePair <string, OpenApiPathItem> path) { ArgumentNullException.ThrowIfNull(validationOptions); var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; foreach (var(key, value) in path.Value.Operations) { if (key != OperationType.Get || (path.Value.Parameters.All(x => x.In != ParameterLocation.Path) && value.Parameters.All(x => x.In != ParameterLocation.Path))) { continue; } var httpStatusCodes = value.Responses.GetHttpStatusCodes(); if (!httpStatusCodes.Contains(HttpStatusCode.NotFound)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation14, $"Missing NotFound response type for operation '{value.GetOperationName()}', required by url parameter.")); } } return(logItems); }
/// <summary> /// Check global parameters. /// </summary> /// <param name="validationOptions">The validation options.</param> /// <param name="globalPathParameterNames">The global path parameter names.</param> /// <param name="path">The path.</param> public static List <LogKeyValueItem> ValidateGlobalParameters( ApiOptionsValidation validationOptions, IEnumerable <string> globalPathParameterNames, KeyValuePair <string, OpenApiPathItem> path) { if (validationOptions == null) { throw new ArgumentNullException(nameof(validationOptions)); } if (globalPathParameterNames == null) { throw new ArgumentNullException(nameof(globalPathParameterNames)); } var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; foreach (var pathParameterName in globalPathParameterNames) { if (!path.Key.Contains(pathParameterName, StringComparison.OrdinalIgnoreCase)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation11, $"Defined global path parameter '{pathParameterName}' does not exist in route '{path.Key}'.")); } } return(logItems); }
/// <summary> /// Check for operations that are not defining parameters, which are present in the path.key. /// </summary> /// <param name="validationOptions">The validation options.</param> /// <param name="path">The path.</param> public static List <LogKeyValueItem> ValidateMissingOperationParameters( ApiOptionsValidation validationOptions, KeyValuePair <string, OpenApiPathItem> path) { if (validationOptions == null) { throw new ArgumentNullException(nameof(validationOptions)); } var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; if (!path.Key.Contains('{', StringComparison.Ordinal) || !path.Key.IsStringFormatParametersBalanced(false)) { return(logItems); } var parameterNamesToCheckAgainst = GetParameterListFromPathKey(path.Key); var allOperationsParametersFromPath = GetAllOperationsParametersFromPath(path.Value.Operations); var distinctOperations = allOperationsParametersFromPath .Select(x => x.Item1) .Distinct() .ToList(); foreach (var parameterName in parameterNamesToCheckAgainst) { var allOperationWithTheMatchingParameterName = allOperationsParametersFromPath .Where(x => x.Item2.Equals(parameterName, StringComparison.OrdinalIgnoreCase)) .ToList(); if (distinctOperations.Count != allOperationWithTheMatchingParameterName.Count) { var operationsWithMissingParameter = allOperationsParametersFromPath .Where(x => string.IsNullOrEmpty(x.Item2)) .Select(x => x.Item1) .ToList(); logItems.Add(operationsWithMissingParameter.Count == 0 ? LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation12, $"The operations in path '{path.Key}' does not define a parameter named '{parameterName}'.") : LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation12, $"The operations '{string.Join(',', operationsWithMissingParameter)}' in path '{path.Key}' does not define a parameter named '{parameterName}'.")); } } return(logItems); }
/// <summary> /// Check for operations with parameters, that are not present in the path.key. /// </summary> /// <param name="validationOptions">The validation options.</param> /// <param name="path">The path.</param> public static List <LogKeyValueItem> ValidateOperationsWithParametersNotPresentInPath( ApiOptionsValidation validationOptions, KeyValuePair <string, OpenApiPathItem> path) { if (validationOptions == null) { throw new ArgumentNullException(nameof(validationOptions)); } var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; var openApiOperationsWithPathParameter = path.Value.Operations.Values .Where(x => x.Parameters.Any(p => p.In == ParameterLocation.Path)) .ToList(); if (!openApiOperationsWithPathParameter.Any()) { return(logItems); } var operationPathParameterNames = new List <string>(); foreach (var openApiOperation in openApiOperationsWithPathParameter) { operationPathParameterNames.AddRange(openApiOperation.Parameters .Where(x => x.In == ParameterLocation.Path) .Select(openApiParameter => openApiParameter.Name)); } if (!operationPathParameterNames.Any()) { return(logItems); } foreach (var operationParameterName in operationPathParameterNames) { if (!path.Key.Contains(operationParameterName, StringComparison.OrdinalIgnoreCase)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation13, $"Defined path parameter '{operationParameterName}' does not exist in route '{path.Key}'.")); } } return(logItems); }
private static List <LogKeyValueItem> ValidateSchemaModelPropertyNameCasing( ApiOptionsValidation validationOptions, string key, OpenApiSchema schema) { var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; if (!key.IsCasingStyleValid(validationOptions.ModelPropertyNameCasingStyle)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema07, $"Object '{schema.Title}' with property '{key}' is not using {validationOptions.ModelPropertyNameCasingStyle}.")); } return(logItems); }
private static List <LogKeyValueItem> ValidateOperationsParametersAndResponses( ApiOptionsValidation validationOptions, Dictionary <string, OpenApiPathItem> .ValueCollection paths) { var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; foreach (var path in paths) { foreach (var(_, value) in path.Operations) { var httpStatusCodes = value.Responses.GetHttpStatusCodes(); if (httpStatusCodes.Contains(HttpStatusCode.BadRequest) && !value.HasParametersOrRequestBody() && !path.HasParameters()) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation10, $"Contains BadRequest response type for operation '{value.GetOperationName()}', but has no parameters.")); } foreach (var parameter in value.Parameters) { switch (parameter.In) { case ParameterLocation.Path: if (!parameter.Required) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation15, $"Path parameter '{parameter.Name}' for operation '{value.GetOperationName()}' is missing required=true.")); } if (parameter.Schema.Nullable) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation16, $"Path parameter '{parameter.Name}' for operation '{value.GetOperationName()}' must not be nullable.")); } break; case ParameterLocation.Query: break; } } } } return(logItems); }
private static List <LogKeyValueItem> ValidateSchemaModelNameCasing( ApiOptionsValidation validationOptions, OpenApiSchema schema) { var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; var modelName = schema.GetModelName(false); if (!modelName.IsCasingStyleValid(validationOptions.ModelNameCasingStyle)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema06, $"Object '{modelName}' is not using {validationOptions.ModelNameCasingStyle}.")); } return(logItems); }
private static List <LogKeyValueItem> ValidateServers( ApiOptionsValidation validationOptions, IEnumerable <OpenApiServer> servers) { var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; var server = servers.FirstOrDefault(); if (server is not null && !IsServerUrlValid(server.Url)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Server01, "Invalid server url.")); } return(logItems); }
private static List <LogKeyValueItem> ValidatePathsAndOperations( ApiOptionsValidation validationOptions, OpenApiPaths paths) { var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; foreach (var path in paths) { if (!path.Key.IsStringFormatParametersBalanced(false)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Path01, $"Path parameters are not well-formatted for '{path.Key}'.")); } var globalPathParameterNames = path.Value.Parameters .Where(x => x.In == ParameterLocation.Path) .Select(x => x.Name) .ToList(); if (globalPathParameterNames.Any()) { logItems.AddRange(ValidatePathsAndOperationsHelper.ValidateGlobalParameters(validationOptions, globalPathParameterNames, path)); } else { logItems.AddRange(ValidatePathsAndOperationsHelper.ValidateMissingOperationParameters(validationOptions, path)); logItems.AddRange(ValidatePathsAndOperationsHelper.ValidateOperationsWithParametersNotPresentInPath(validationOptions, path)); } logItems.AddRange(ValidatePathsAndOperationsHelper.ValidateGetOperations(validationOptions, path)); } return(logItems); }
private static List <LogKeyValueItem> ValidateSchemas( ApiOptionsValidation validationOptions, ICollection <OpenApiSchema> schemas) { var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; foreach (var schema in schemas) { switch (schema.Type) { case OpenApiDataTypeConstants.Array: { if (string.IsNullOrEmpty(schema.Title)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema01, $"Missing title on array type '{schema.Reference.ReferenceV3}'.")); } else if (schema.Title.IsFirstCharacterLowerCase()) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema02, $"Title on array type '{schema.Title}' is not starting with uppercase.")); } logItems.AddRange(ValidateSchemaModelNameCasing(validationOptions, schema)); break; } case OpenApiDataTypeConstants.Object: { if (string.IsNullOrEmpty(schema.Title)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema03, $"Missing title on object type '{schema.Reference.ReferenceV3}'.")); } else if (schema.Title.IsFirstCharacterLowerCase()) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema04, $"Title on object type '{schema.Title}' is not starting with uppercase.")); } foreach (var(key, value) in schema.Properties) { if (value.Nullable && schema.Required.Contains(key)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema08, $"Nullable property '{key}' must not be present in required property list in type '{schema.Reference.ReferenceV3}'.")); } switch (value.Type) { case OpenApiDataTypeConstants.Object: { if (!value.IsObjectReferenceTypeDeclared()) { logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Schema10, $"Implicit object definition on property '{key}' in type '{schema.Reference.ReferenceV3}' is not supported.")); } break; } case OpenApiDataTypeConstants.Array: { if (value.Items == null) { logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Schema11, $"Not specifying a data type for array property '{key}' in type '{schema.Reference.ReferenceV3}' is not supported.")); } else { if (value.Items.Type == null) { logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Schema09, $"Not specifying a data type for array property '{key}' in type '{schema.Reference.ReferenceV3}' is not supported.")); } if (value.Items.Type != null && !value.IsArrayReferenceTypeDeclared() && !value.IsItemsOfSimpleDataType()) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema05, $"Implicit object definition on property '{key}' in array type '{schema.Reference.ReferenceV3}' is not supported.")); } } break; } } logItems.AddRange(ValidateSchemaModelPropertyNameCasing(validationOptions, key, schema)); } logItems.AddRange(ValidateSchemaModelNameCasing(validationOptions, schema)); break; } } } return(logItems); }
public static List <LogKeyValueItem> ValidateDocument(OpenApiDocument apiDocument, ApiOptionsValidation validationOptions) { if (apiDocument == null) { throw new ArgumentNullException(nameof(apiDocument)); } if (validationOptions == null) { throw new ArgumentNullException(nameof(validationOptions)); } var logItems = new List <LogKeyValueItem>(); logItems.AddRange(ValidateSchemas(validationOptions, apiDocument.Components.Schemas.Values)); logItems.AddRange(ValidateOperations(validationOptions, apiDocument.Paths, apiDocument.Components.Schemas)); logItems.AddRange(ValidatePathsAndOperations(validationOptions, apiDocument.Paths)); logItems.AddRange(ValidateOperationsParametersAndResponses(validationOptions, apiDocument.Paths.Values)); return(logItems); }
private static List <LogKeyValueItem> ValidateOperations( ApiOptionsValidation validationOptions, OpenApiPaths paths, IDictionary <string, OpenApiSchema> modelSchemas) { var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; foreach (var(pathKey, pathValue) in paths) { foreach (var(operationKey, operationValue) in pathValue.Operations) { if (string.IsNullOrEmpty(operationValue.OperationId)) { logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Operation01, $"Missing OperationId in path '{operationKey} # {pathKey}'.")); } else { if (!operationValue.OperationId.IsCasingStyleValid(validationOptions.OperationIdCasingStyle)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation02, $"OperationId '{operationValue.OperationId}' is not using {validationOptions.OperationIdCasingStyle}.")); } if (operationKey == OperationType.Get) { if (!operationValue.OperationId.StartsWith("Get", StringComparison.OrdinalIgnoreCase) && !operationValue.OperationId.StartsWith("List", StringComparison.OrdinalIgnoreCase)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation03, $"OperationId should start with the prefix 'Get' or 'List' for operation '{operationValue.GetOperationName()}'.")); } } else if (operationKey == OperationType.Post) { if (operationValue.OperationId.StartsWith("Delete", StringComparison.OrdinalIgnoreCase)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation04, $"OperationId should not start with the prefix 'Delete' for operation '{operationValue.GetOperationName()}'.")); } } else if (operationKey == OperationType.Put) { if (!operationValue.OperationId.StartsWith("Update", StringComparison.OrdinalIgnoreCase)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation05, $"OperationId should start with the prefix 'Update' for operation '{operationValue.GetOperationName()}'.")); } } else if (operationKey == OperationType.Patch) { if (!operationValue.OperationId.StartsWith("Patch", StringComparison.OrdinalIgnoreCase) && !operationValue.OperationId.StartsWith("Update", StringComparison.OrdinalIgnoreCase)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation06, $"OperationId should start with the prefix 'Update' for operation '{operationValue.GetOperationName()}'.")); } } else if (operationKey == OperationType.Delete && !operationValue.OperationId.StartsWith("Delete", StringComparison.OrdinalIgnoreCase) && !operationValue.OperationId.StartsWith("Remove", StringComparison.OrdinalIgnoreCase)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation07, $"OperationId should start with the prefix 'Delete' for operation '{operationValue.GetOperationName()}'.")); } } } foreach (var(_, value) in pathValue.Operations) { var modelSchema = value.GetModelSchema(); if (modelSchema != null) { if (value.OperationId.EndsWith("s", StringComparison.Ordinal)) { if (!IsModelOfTypeArray(modelSchema, modelSchemas)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation08, $"OperationId '{value.GetOperationName()}' is not singular - Response model is defined as a single item.")); } } else { if (IsModelOfTypeArray(modelSchema, modelSchemas)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation09, $"OperationId '{value.GetOperationName()}' is not pluralized - Response model is defined as an array.")); } } } } } return(logItems); }
private static List <LogKeyValueItem> ValidateOperationsParametersAndResponses( ApiOptionsValidation validationOptions, Dictionary <string, OpenApiPathItem> .ValueCollection paths) { var logItems = new List <LogKeyValueItem>(); var logCategory = validationOptions.StrictMode ? LogCategoryType.Error : LogCategoryType.Warning; foreach (var path in paths) { foreach (var(operationType, value) in path.Operations) { var httpStatusCodes = value.Responses.GetHttpStatusCodes(); if (httpStatusCodes.Contains(HttpStatusCode.BadRequest) && !value.HasParametersOrRequestBody() && !path.HasParameters()) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation10, $"Contains BadRequest response type for operation '{value.OperationId}', but has no parameters.")); } if (httpStatusCodes.Contains(HttpStatusCode.OK) && httpStatusCodes.Contains(HttpStatusCode.Created)) { // We do not support both 200 and 201, since our ActionResult - implicit operators only supports 1 type. logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation18, $"The operation '{value.OperationId}' contains both 200 and 201, which is not supported.")); } if (value.HasParametersOrRequestBody()) { var schema = value.RequestBody?.Content.GetSchemaByFirstMediaType(); if (schema is not null && string.IsNullOrEmpty(schema.GetModelName()) && !schema.IsFormatTypeBinary() && !schema.HasItemsWithFormatTypeBinary()) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation17, $"RequestBody is defined with inline model for operation '{value.OperationId}' - only reference to component-schemas are supported.")); } } foreach (var parameter in value.Parameters) { switch (parameter.In) { case ParameterLocation.Path: if (!parameter.Required) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation15, $"Path parameter '{parameter.Name}' for operation '{value.OperationId}' is missing required=true.")); } if (parameter.Schema.Nullable) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation16, $"Path parameter '{parameter.Name}' for operation '{value.OperationId}' must not be nullable.")); } break; case ParameterLocation.Query: break; } } } } return(logItems); }
public static List <LogKeyValueItem> Validate(Tuple <OpenApiDocument, OpenApiDiagnostic, FileInfo> apiDocument, ApiOptionsValidation validationOptions) { if (apiDocument == null) { throw new ArgumentNullException(nameof(apiDocument)); } if (validationOptions == null) { throw new ArgumentNullException(nameof(validationOptions)); } var logItems = new List <LogKeyValueItem>(); if (apiDocument.Item2.SpecificationVersion == OpenApiSpecVersion.OpenApi2_0) { logItems.Add(LogItemHelper.Create(LogCategoryType.Error, "#", "OpenApi 2.x is not supported.")); return(logItems); } foreach (var diagnosticError in apiDocument.Item2.Errors) { if (diagnosticError.Message.EndsWith("#/components/schemas", StringComparison.Ordinal)) { continue; } var description = string.IsNullOrEmpty(diagnosticError.Pointer) ? $"{diagnosticError.Message}" : $"{diagnosticError.Message} <#> {diagnosticError.Pointer}"; logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.OpenApiCore, description)); } logItems.AddRange(OpenApiDocumentValidationHelper.ValidateDocument(apiDocument.Item1, validationOptions)); return(logItems); }