public List <LogKeyValueItem> SetPropertiesAfterValidationsOfProjectReferencesPathAndFiles() { var logItems = new List <LogKeyValueItem>(); if (ApiProjectSrcPath.Exists) { var files = Directory.GetFiles(ApiProjectSrcPath.FullName, "ApiRegistration.cs", SearchOption.AllDirectories); if (files.Length == 1) { ApiProjectSrcPath = new FileInfo(files[0]).Directory !; files = Directory.GetFiles(ApiProjectSrcPath.FullName, "*.csproj", SearchOption.AllDirectories); if (files.Length == 1) { ApiProjectSrcCsProj = new FileInfo(files[0]); } } } if (ApiProjectSrcCsProj == null || !ApiProjectSrcCsProj.Exists) { logItems.Add(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.ProjectHostGenerated04, "Can't find API .csproj file")); } return(logItems); }
/// <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); }
public void Create() { // Act var actual = LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.Operation04, "Hallo world"); // Assert Assert.NotNull(actual); Assert.Equal("Key: CR0204, Value: Operation, LogCategory: Error, Description: Hallo world", actual.ToString()); }
private static LogKeyValueItem ValidateVersioning(ApiProjectOptions apiProjectOptions) { if (apiProjectOptions == null) { throw new ArgumentNullException(nameof(apiProjectOptions)); } if (!Directory.Exists(apiProjectOptions.PathForSrcGenerate.FullName)) { return(LogItemHelper.Create(LogCategoryType.Information, ValidationRuleNameConstants.ProjectApiGenerated01, "Old project don't exist.")); } var apiGeneratedFile = Path.Combine(apiProjectOptions.PathForSrcGenerate.FullName, "ApiRegistration.cs"); if (!File.Exists(apiGeneratedFile)) { return(LogItemHelper.Create(LogCategoryType.Information, ValidationRuleNameConstants.ProjectApiGenerated02, "Old ApiRegistration.cs in project don't exist.")); } var lines = File.ReadLines(apiGeneratedFile).ToList(); const string toolName = "ApiGenerator"; var newVersion = GenerateHelper.GetAtcToolVersion(); foreach (var line in lines) { var indexOfToolName = line.IndexOf(toolName !, StringComparison.Ordinal); if (indexOfToolName == -1) { continue; } var oldVersion = line.Substring(indexOfToolName + toolName !.Length); if (oldVersion.EndsWith('.')) { oldVersion = oldVersion.Substring(0, oldVersion.Length - 1); } if (!Version.TryParse(oldVersion, out var oldVersionResult)) { return(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.ProjectApiGenerated03, "Existing project version is invalid.")); } if (newVersion >= oldVersionResult) { return(LogItemHelper.Create(LogCategoryType.Information, ValidationRuleNameConstants.ProjectApiGenerated04, "The generate project version is the same or newer.")); } return(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.ProjectApiGenerated05, "Existing project version is never than this tool version.")); } return(LogItemHelper.Create(LogCategoryType.Error, ValidationRuleNameConstants.ProjectApiGenerated06, "Existing project did not contain a version.")); }
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); }
/// <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> /// <returns>List of possible validation errors</returns> public static List <LogKeyValueItem> ValidateMissingOperationParameters( ApiOptionsValidation validationOptions, KeyValuePair <string, OpenApiPathItem> path) { ArgumentNullException.ThrowIfNull(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(StringComparer.Ordinal) .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); }
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(ensureFirstCharacterToUpper: false); if (!modelName.IsCasingStyleValid(validationOptions.ModelNameCasingStyle)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Schema06, $"Object '{modelName}' is not using {validationOptions.ModelNameCasingStyle}.")); } 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> /// <returns>List of possible validation errors</returns> public static List <LogKeyValueItem> ValidateOperationsWithParametersNotPresentInPath( ApiOptionsValidation validationOptions, KeyValuePair <string, OpenApiPathItem> path) { ArgumentNullException.ThrowIfNull(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> 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); }
/// <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> /// <returns>List of possible validation errors</returns> public static List <LogKeyValueItem> ValidateGlobalParameters( ApiOptionsValidation validationOptions, IEnumerable <string> globalPathParameterNames, KeyValuePair <string, OpenApiPathItem> path) { ArgumentNullException.ThrowIfNull(validationOptions); ArgumentNullException.ThrowIfNull(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); }
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, IEnumerable <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) { 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 is 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 is 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 is not null && !value.IsArrayReferenceTypeDeclared() && !value.HasItemsWithSimpleDataType() && !value.HasItemsWithFormatTypeBinary()) { 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); }
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); }
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.OperationId}'.")); } } 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.OperationId}'.")); } } 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.OperationId}'.")); } } 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.OperationId}'.")); } } 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.OperationId}'.")); } } } foreach (var(operationKey, operationValue) in pathValue.Operations) { // Validate Response Schema var responseModelSchema = operationValue.GetModelSchemaFromResponse(); if (responseModelSchema is not null) { if (operationValue.IsOperationIdPluralized(operationKey)) { if (!IsModelOfTypeArray(responseModelSchema, modelSchemas)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation08, $"OperationId '{operationValue.OperationId}' is not singular - Response model is defined as a single item.")); } } else { if (IsModelOfTypeArray(responseModelSchema, modelSchemas)) { logItems.Add(LogItemHelper.Create(logCategory, ValidationRuleNameConstants.Operation09, $"OperationId '{operationValue.OperationId}' is not pluralized - Response model is defined as an array.")); } } } //// TO-DO Validate RequestBody Schema } } return(logItems); }