diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs new file mode 100644 index 000000000..757f419df --- /dev/null +++ b/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations; + +namespace Microsoft.OpenApi.Extensions +{ + /// + /// Extension methods that apply across all OpenAPIElements + /// + public static class OpenApiElementExtensions + { + /// + /// Validate element and all child elements + /// + /// + /// + /// + public static IEnumerable Validate(this IOpenApiElement element) { + var validator = new OpenApiValidator(); + var walker = new OpenApiWalker(validator); + walker.Walk(element); + return validator.Errors; + } + } +} diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 2b77e680a..4d0dd1fd5 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -96,6 +96,15 @@ internal static string IndentationLevelInvalid { } } + /// + /// Looks up a localized string similar to The input item should be in type of '{0}'.. + /// + internal static string InputItemShouldBeType { + get { + return ResourceManager.GetString("InputItemShouldBeType", resourceCulture); + } + } + /// /// Looks up a localized string similar to The active scope must be an object scope for property name '{0}' to be written.. /// @@ -239,5 +248,68 @@ internal static string SourceExpressionHasInvalidFormat { return ResourceManager.GetString("SourceExpressionHasInvalidFormat", resourceCulture); } } + + /// + /// Looks up a localized string similar to Can not find visitor type registered for type '{0}'.. + /// + internal static string UnknownVisitorType { + get { + return ResourceManager.GetString("UnknownVisitorType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The key '{0}' in '{1}' of components MUST match the regular expression '{2}'.. + /// + internal static string Validation_ComponentsKeyMustMatchRegularExpr { + get { + return ResourceManager.GetString("Validation_ComponentsKeyMustMatchRegularExpr", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The extension name '{0}' in '{1}' object MUST begin with 'x-'.. + /// + internal static string Validation_ExtensionNameMustBeginWithXDash { + get { + return ResourceManager.GetString("Validation_ExtensionNameMustBeginWithXDash", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The field '{0}' in '{1}' object is REQUIRED.. + /// + internal static string Validation_FieldIsRequired { + get { + return ResourceManager.GetString("Validation_FieldIsRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The path item name '{0}' MUST begin with a slash.. + /// + internal static string Validation_PathItemMustBeginWithSlash { + get { + return ResourceManager.GetString("Validation_PathItemMustBeginWithSlash", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The same rule cannot be in the same rule set twice.. + /// + internal static string Validation_RuleAddTwice { + get { + return ResourceManager.GetString("Validation_RuleAddTwice", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The string '{0}' MUST be in the format of an email address.. + /// + internal static string Validation_StringMustBeEmailAddress { + get { + return ResourceManager.GetString("Validation_StringMustBeEmailAddress", resourceCulture); + } + } } } diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx index bfffc638c..e3f232c76 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -129,6 +129,9 @@ Indentation level cannot be lower than 0. + + The input item should be in type of '{0}'. + The active scope must be an object scope for property name '{0}' to be written. @@ -177,4 +180,25 @@ The source expression '{0}' has invalid format. + + Can not find visitor type registered for type '{0}'. + + + The key '{0}' in '{1}' of components MUST match the regular expression '{2}'. + + + The extension name '{0}' in '{1}' object MUST begin with 'x-'. + + + The field '{0}' in '{1}' object is REQUIRED. + + + The path item name '{0}' MUST begin with a slash. + + + The same rule cannot be in the same rule set twice. + + + The string '{0}' MUST be in the format of an email address. + \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Services/OpenApiValidator.cs b/src/Microsoft.OpenApi/Services/OpenApiValidator.cs deleted file mode 100644 index 25d9b4d1b..000000000 --- a/src/Microsoft.OpenApi/Services/OpenApiValidator.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -using System.Collections.Generic; -using Microsoft.OpenApi.Exceptions; -using Microsoft.OpenApi.Models; - -namespace Microsoft.OpenApi.Services -{ - /// - /// Class containing logic to validate an Open API document object. - /// - public class OpenApiValidator : OpenApiVisitorBase - { - /// - /// Exceptions related to this validation. - /// - public List Exceptions { get; } = new List(); - - /// - /// Visit Open API Response element. - /// - /// Response element. - public override void Visit(OpenApiResponse response) - { - if (string.IsNullOrEmpty(response.Description)) - { - Exceptions.Add(new OpenApiException("Response must have a description")); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index 1a20c67ca..0a0bc2416 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -13,143 +13,236 @@ namespace Microsoft.OpenApi.Services public abstract class OpenApiVisitorBase { /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiDocument doc) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiInfo info) { } /// - /// Validates list of + /// Visits + /// + public virtual void Visit(OpenApiContact contact) + { + } + + + /// + /// Visits + /// + public virtual void Visit(OpenApiLicense license) + { + } + + /// + /// Visits list of /// public virtual void Visit(IList servers) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiServer server) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiPaths paths) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiPathItem pathItem) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiServerVariable serverVariable) { } /// - /// Validates the operations. + /// Visits the operations. /// public virtual void Visit(IDictionary operations) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiOperation operation) { } /// - /// Validates list of + /// Visits list of /// public virtual void Visit(IList parameters) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiParameter parameter) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiRequestBody requestBody) { } /// - /// Validates responses. + /// Visits responses. /// public virtual void Visit(IDictionary responses) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiResponse response) { } /// - /// Validates media type content. + /// Visits + /// + public virtual void Visit(OpenApiResponses response) + { + } + + /// + /// Visits media type content. /// public virtual void Visit(IDictionary content) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiMediaType mediaType) { } /// - /// Validates the examples. + /// Visits + /// + public virtual void Visit(OpenApiEncoding encoding) + { + } + + /// + /// Visits the examples. /// public virtual void Visit(IDictionary examples) { } /// - /// Validates + /// Visits + /// + public virtual void Visit(OpenApiComponents components) + { + } + + + /// + /// Visits + /// + public virtual void Visit(OpenApiExternalDocs externalDocs) + { + } + + /// + /// Visits /// public virtual void Visit(OpenApiSchema schema) { } /// - /// Validates the links. + /// Visits the links. /// public virtual void Visit(IDictionary links) { } /// - /// Validates + /// Visits /// public virtual void Visit(OpenApiLink link) { } + + /// + /// Visits + /// + public virtual void Visit(OpenApiCallback callback) + { + } + + /// + /// Visits + /// + public virtual void Visit(OpenApiTag tag) + { + } + + /// + /// Visits + /// + public virtual void Visit(OpenApiHeader tag) + { + } + + /// + /// Visits + /// + public virtual void Visit(OpenApiOAuthFlow openApiOAuthFlow) + { + } + + /// + /// Visits + /// + public virtual void Visit(OpenApiSecurityRequirement securityRequirement) + { + } + + /// + /// Visits list of + /// + public virtual void Visit(IList openApiTags) + { + } + + /// + /// Visits + /// + public virtual void Visit(IOpenApiExtensible openApiExtensible) + { + } } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 66d4e319c..23f898f85 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using Microsoft.OpenApi.Models; +using System; using System.Collections.Generic; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Interfaces; namespace Microsoft.OpenApi.Services { @@ -22,81 +24,290 @@ public OpenApiWalker(OpenApiVisitorBase visitor) } /// - /// Walks through the and validates each element. + /// Visits list of and child objects /// - /// + /// OpenApiDocument to be walked public void Walk(OpenApiDocument doc) { _visitor.Visit(doc); - _visitor.Visit(doc.Info); - _visitor.Visit(doc.Servers); - if (doc.Servers != null) + Walk(doc.Info); + Walk(doc.Servers); + Walk(doc.Paths); + Walk(doc.Components); + Walk(doc.ExternalDocs); + Walk(doc.Tags); + Walk(doc as IOpenApiExtensible); + } + + /// + /// Visits list of and child objects + /// + /// + internal void Walk(IList tags) + { + _visitor.Visit(tags); + + foreach (var tag in tags) + { + Walk(tag); + } + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiExternalDocs externalDocs) + { + _visitor.Visit(externalDocs); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiComponents components) + { + _visitor.Visit(components); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiPaths paths) + { + _visitor.Visit(paths); + foreach (var pathItem in paths.Values) { - foreach (var server in doc.Servers) + Walk(pathItem); + } + } + + /// + /// Visits list of and child objects + /// + /// + internal void Walk(IList servers) + { + _visitor.Visit(servers); + + // Visit Servers + if (servers != null) + { + foreach (var server in servers) { - _visitor.Visit(server); - foreach (var variable in server.Variables.Values) - { - _visitor.Visit(variable); - } + Walk(server); } } + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiInfo info) + { + _visitor.Visit(info); + Walk(info.Contact); + Walk(info.License); + Walk(info as IOpenApiExtensible); + } - _visitor.Visit(doc.Paths); - foreach (var pathItem in doc.Paths.Values) + /// + /// Visits dictionary of extensions + /// + /// + internal void Walk(IOpenApiExtensible openApiExtensible) + { + _visitor.Visit(openApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiLicense license) + { + _visitor.Visit(license); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiContact contact) + { + _visitor.Visit(contact); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiTag tag) + { + _visitor.Visit(tag); + _visitor.Visit(tag.ExternalDocs); + _visitor.Visit(tag as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiServer server) + { + _visitor.Visit(server); + foreach (var variable in server.Variables.Values) + { + Walk(variable); + } + _visitor.Visit(server as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiServerVariable serverVariable) + { + _visitor.Visit(serverVariable); + _visitor.Visit(serverVariable as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiPathItem pathItem) + { + _visitor.Visit(pathItem); + + Walk(pathItem.Operations); + _visitor.Visit(pathItem as IOpenApiExtensible); + } + + /// + /// Visits dictionary of + /// + /// + internal void Walk(IDictionary operations) + { + _visitor.Visit(operations); + foreach (var operation in operations.Values) + { + Walk(operation); + } + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiOperation operation) + { + _visitor.Visit(operation); + + Walk(operation.Parameters); + Walk(operation.RequestBody); + Walk(operation.Responses); + Walk(operation as IOpenApiExtensible); + } + + /// + /// Visits list of + /// + /// + internal void Walk(IList parameters) + { + if (parameters != null) { - _visitor.Visit(pathItem); - _visitor.Visit(pathItem.Operations); - foreach (var operation in pathItem.Operations.Values) + _visitor.Visit(parameters); + foreach (var parameter in parameters) { - _visitor.Visit(operation); - if (operation.Parameters != null) - { - _visitor.Visit(operation.Parameters); - foreach (var parameter in operation.Parameters) - { - _visitor.Visit(parameter); - } - } - - if (operation.RequestBody != null) - { - _visitor.Visit(operation.RequestBody); - - if (operation.RequestBody.Content != null) - { - WalkContent(operation.RequestBody.Content); - } - } - - if (operation.Responses != null) - { - _visitor.Visit(operation.Responses); - - foreach (var response in operation.Responses.Values) - { - _visitor.Visit(response); - WalkContent(response.Content); - - if (response.Links != null) - { - _visitor.Visit(response.Links); - foreach (var link in response.Links.Values) - { - _visitor.Visit(link); - } - } - } - } + Walk(parameter); } } } /// - /// Walks through each media type in content and validates. + /// Visits and child objects + /// + /// + internal void Walk(OpenApiParameter parameter) + { + _visitor.Visit(parameter); + Walk(parameter.Schema); + Walk(parameter.Content); + Walk(parameter as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiResponses responses) + { + if (responses != null) + { + _visitor.Visit(responses); + + foreach (var response in responses.Values) + { + Walk(response); + } + + Walk(responses as IOpenApiExtensible); + } + } + + /// + /// Visits and child objects /// - private void WalkContent(IDictionary content) + /// + internal void Walk(OpenApiResponse response) + { + _visitor.Visit(response); + Walk(response.Content); + + if (response.Links != null) + { + _visitor.Visit(response.Links); + foreach (var link in response.Links.Values) + { + _visitor.Visit(link); + } + } + + _visitor.Visit(response as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiRequestBody requestBody) + { + if (requestBody != null) + { + _visitor.Visit(requestBody); + + if (requestBody.Content != null) + { + Walk(requestBody.Content); + } + + Walk(requestBody as IOpenApiExtensible); + } + } + + /// + /// Visits dictionary of + /// + /// + internal void Walk(IDictionary content) { if (content == null) { @@ -106,10 +317,208 @@ private void WalkContent(IDictionary content) _visitor.Visit(content); foreach (var mediaType in content.Values) { - _visitor.Visit(mediaType); - _visitor.Visit(mediaType.Examples); - _visitor.Visit(mediaType.Schema); + Walk(mediaType); + } + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiMediaType mediaType) + { + _visitor.Visit(mediaType); + + Walk(mediaType.Examples); + Walk(mediaType.Schema); + Walk(mediaType.Encoding); + Walk(mediaType as IOpenApiExtensible); + } + + /// + /// Visits dictionary of + /// + /// + internal void Walk(IDictionary encoding) + { + foreach (var item in encoding.Values) + { + _visitor.Visit(item); + } + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiEncoding encoding) + { + _visitor.Visit(encoding); + Walk(encoding as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiSchema schema) + { + _visitor.Visit(schema); + Walk(schema.ExternalDocs); + Walk(schema as IOpenApiExtensible); + } + + /// + /// Visits dictionary of + /// + /// + internal void Walk(IDictionary examples) + { + _visitor.Visit(examples); + foreach (var example in examples.Values) + { + Walk(example); + } + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiExample example) + { + _visitor.Visit(example); + Walk(example as IOpenApiExtensible); + } + + /// + /// Visits the list of and child objects + /// + /// + internal void Walk(IList examples) + { + foreach (var item in examples) + { + _visitor.Visit(item); + } + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiOAuthFlows flows) + { + _visitor.Visit(flows); + Walk(flows as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiOAuthFlow oAuthFlow) + { + _visitor.Visit(oAuthFlow); + Walk(oAuthFlow as IOpenApiExtensible); + } + + /// + /// Visits dictionary of and child objects + /// + /// + internal void Walk(IDictionary links) + { + foreach (var item in links) + { + _visitor.Visit(item.Value); + } + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiLink link) + { + _visitor.Visit(link); + Walk(link.Server); + Walk(link as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiHeader header) + { + _visitor.Visit(header); + Walk(header.Content); + Walk(header.Example); + Walk(header.Examples); + Walk(header.Schema); + Walk(header as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiSecurityRequirement securityRequirement) + { + _visitor.Visit(securityRequirement); + Walk(securityRequirement as IOpenApiExtensible); + } + + /// + /// Visits and child objects + /// + /// + internal void Walk(OpenApiSecurityScheme securityScheme) + { + _visitor.Visit(securityScheme); + Walk(securityScheme as IOpenApiExtensible); + } + + /// + /// Walk IOpenApiElement + /// + /// + internal void Walk(IOpenApiElement element) + { + switch(element) + { + case OpenApiDocument e: Walk(e); break; + case OpenApiLicense e: Walk(e); break; + case OpenApiInfo e: Walk(e); break; + case OpenApiComponents e: Walk(e); break; + case OpenApiContact e: Walk(e); break; + case OpenApiCallback e: Walk(e); break; + case OpenApiEncoding e: Walk(e); break; + case OpenApiExample e: Walk(e); break; + case IDictionary e: Walk(e); break; + case OpenApiExternalDocs e: Walk(e); break; + case OpenApiHeader e: Walk(e); break; + case OpenApiLink e: Walk(e); break; + case IDictionary e: Walk(e); break; + case OpenApiMediaType e: Walk(e); break; + case OpenApiOAuthFlows e: Walk(e); break; + case OpenApiOAuthFlow e: Walk(e); break; + case OpenApiOperation e: Walk(e); break; + case OpenApiParameter e: Walk(e); break; + case OpenApiRequestBody e: Walk(e); break; + case OpenApiResponse e: Walk(e); break; + case OpenApiSchema e: Walk(e); break; + case OpenApiSecurityRequirement e: Walk(e); break; + case OpenApiSecurityScheme e: Walk(e); break; + case OpenApiServer e: Walk(e); break; + case OpenApiServerVariable e: Walk(e); break; + case OpenApiTag e: Walk(e); break; + case IList e: Walk(e); break; + case OpenApiXml e: Walk(e); break; + case IOpenApiExtensible e: Walk(e); break; } } + } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs new file mode 100644 index 000000000..468544b94 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations; + +namespace Microsoft.OpenApi.Validations +{ + /// + /// Class containing dispatchers to execute validation rules on for Open API document. + /// + public class OpenApiValidator : OpenApiVisitorBase + { + readonly ValidationRuleSet _ruleSet; + readonly ValidationContext _context; + + /// + /// Create a vistor that will validate an OpenAPIDocument + /// + /// + public OpenApiValidator(ValidationRuleSet ruleSet = null) + { + _ruleSet = ruleSet ?? ValidationRuleSet.DefaultRuleSet; + _context = new ValidationContext(_ruleSet); + } + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiDocument item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiInfo item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiContact item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiComponents item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiResponse item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiResponses item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiExternalDocs item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiLicense item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiOAuthFlow item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiTag item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiSchema item) => Validate(item); + + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiServer item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiEncoding item) => Validate(item); + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiCallback item) => Validate(item); + + + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(IOpenApiExtensible item) => Validate(item); + + /// + /// Errors accumulated while validating OpenAPI elements + /// + public IEnumerable Errors => _context.Errors; + + private void Validate(T item) + { + if (item == null) return; // Required fields should be checked by higher level objects + var rules = _ruleSet.Where(r => r.ElementType == typeof(T)); + foreach (var rule in rules) + { + rule.Evaluate(_context, item); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs new file mode 100644 index 000000000..11f0f7ada --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiComponentsRules + { + /// + /// The key regex. + /// + public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$"); + + /// + /// All the fixed fields declared above are objects + /// that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$. + /// + public static ValidationRule KeyMustBeRegularExpression => + new ValidationRule( + (context, components) => + { + ValidateKeys(context, components.Schemas?.Keys, "schemas"); + + ValidateKeys(context, components.Responses?.Keys, "responses"); + + ValidateKeys(context, components.Parameters?.Keys, "parameters"); + + ValidateKeys(context, components.Examples?.Keys, "examples"); + + ValidateKeys(context, components.RequestBodies?.Keys, "requestBodies"); + + ValidateKeys(context, components.Headers?.Keys, "headers"); + + ValidateKeys(context, components.SecuritySchemes?.Keys, "securitySchemes"); + + ValidateKeys(context, components.Links?.Keys, "links"); + + ValidateKeys(context, components.Callbacks?.Keys, "callbacks"); + }); + + private static void ValidateKeys(ValidationContext context, IEnumerable keys, string component) + { + if (keys == null) + { + return; + } + + foreach (var key in keys) + { + if (!KeyRegex.IsMatch(key)) + { + ValidationError error = new ValidationError(ErrorReason.Format, context.PathString, + string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, component, KeyRegex.ToString())); + context.AddError(error); + } + } + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs new file mode 100644 index 000000000..e4a239198 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + internal static class OpenApiContactRules + { + /// + /// Email field MUST be email address. + /// + public static ValidationRule EmailMustBeEmailFormat => + new ValidationRule( + (context, item) => + { + context.Push("email"); + if (item != null && item.Email != null) + { + if (!item.Email.IsEmailAddress()) + { + ValidationError error = new ValidationError(ErrorReason.Format, context.PathString, + String.Format(SRResource.Validation_StringMustBeEmailAddress, item.Email)); + context.AddError(error); + } + } + context.Pop(); + }); + + /// + /// Url field MUST be url format. + /// + public static ValidationRule UrlMustBeUrlFormat => + new ValidationRule( + (context, item) => + { + context.Push("url"); + if (item != null && item.Url != null) + { + // TODO: + } + context.Pop(); + }); + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs new file mode 100644 index 000000000..030410123 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + internal static class OpenApiDocumentRules + { + /// + /// The Info field is required. + /// + public static ValidationRule FieldIsRequired => + new ValidationRule( + (context, item) => + { + // info + context.Push("info"); + if (item.Info == null) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "info", "document")); + context.AddError(error); + } + context.Pop(); + + // paths + context.Push("paths"); + if (item.Paths == null) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "paths", "document")); + context.AddError(error); + } + context.Pop(); + }); + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs new file mode 100644 index 000000000..db2a9711a --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExtensionRules.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiExtensibleRules + { + /// + /// Extension name MUST start with "x-". + /// + public static ValidationRule ExtensionNameMustStartWithXDash => + new ValidationRule( + (context, item) => + { + context.Push("extensions"); + foreach (var extensible in item.Extensions) + { + if (!extensible.Key.StartsWith("x-")) + { + ValidationError error = new ValidationError(ErrorReason.Format, context.PathString, + String.Format(SRResource.Validation_ExtensionNameMustBeginWithXDash, extensible.Key, context.PathString)); + context.AddError(error); + } + } + context.Pop(); + }); + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs new file mode 100644 index 000000000..2c42cbb47 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiExternalDocsRules.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + internal static class OpenApiExternalDocsRules + { + /// + /// Validate the field is required. + /// + public static ValidationRule FieldIsRequired => + new ValidationRule( + (context, item) => + { + // url + context.Push("url"); + if (item.Url == null) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "url", "External Documentation")); + context.AddError(error); + } + context.Pop(); + }); + + // add more rule. + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs new file mode 100644 index 000000000..d77f28898 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + internal static class OpenApiInfoRules + { + /// + /// Validate the field is required. + /// + public static ValidationRule FieldIsRequired => + new ValidationRule( + (context, item) => + { + // title + context.Push("title"); + if (String.IsNullOrEmpty(item.Title)) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "url", "info")); + context.AddError(error); + } + context.Pop(); + + // version + context.Push("version"); + if (String.IsNullOrEmpty(item.Version)) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "version", "info")); + context.AddError(error); + } + context.Pop(); + }); + + // add more rule. + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs new file mode 100644 index 000000000..4f17b4753 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiLicenseRules.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiLicenseRules + { + /// + /// REQUIRED. + /// + public static ValidationRule FieldIsRequired => + new ValidationRule( + (context, license) => + { + context.Push("name"); + if (String.IsNullOrEmpty(license.Name)) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "name", "license")); + context.AddError(error); + } + context.Pop(); + }); + + // add more rules + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs new file mode 100644 index 000000000..dca45f890 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + internal static class OpenApiOAuthFlowRules + { + /// + /// Validate the field is required. + /// + public static ValidationRule FieldIsRequired => + new ValidationRule( + (context, flow) => + { + // authorizationUrl + context.Push("authorizationUrl"); + if (flow.AuthorizationUrl == null) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "authorizationUrl", "OAuth Flow")); + context.AddError(error); + } + context.Pop(); + + // tokenUrl + context.Push("tokenUrl"); + if (flow.TokenUrl == null) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "tokenUrl", "OAuth Flow")); + context.AddError(error); + } + context.Pop(); + + // scopes + context.Push("scopes"); + if (flow.Scopes == null) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "scopes", "OAuth Flow")); + context.AddError(error); + } + context.Pop(); + }); + + // add more rule. + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs new file mode 100644 index 000000000..dbc6eb383 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiPathsRules + { + /// + /// A relative path to an individual endpoint. The field name MUST begin with a slash. + /// + public static ValidationRule PathNameMustBeginWithSlash => + new ValidationRule( + (context, item) => + { + foreach (var pathName in item.Keys) + { + context.Push(pathName); + + if (string.IsNullOrEmpty(pathName)) + { + // Add the error message + // context.Add(...); + } + + if (!pathName.StartsWith("/")) + { + ValidationError error = new ValidationError(ErrorReason.Format, context.PathString, + string.Format(SRResource.Validation_PathItemMustBeginWithSlash, pathName)); + context.AddError(error); + } + + context.Pop(); + } + }); + + // add more rules + } +} \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs new file mode 100644 index 000000000..1c6f57b16 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponseRules.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + internal static class OpenApiResponseRules + { + /// + /// Validate the field is required. + /// + public static ValidationRule FieldIsRequired => + new ValidationRule( + (context, response) => + { + // description + context.Push("description"); + if (String.IsNullOrEmpty(response.Description)) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "description", "response")); + context.AddError(error); + } + context.Pop(); + }); + + // add more rule. + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiRuleAttribute.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiRuleAttribute.cs new file mode 100644 index 000000000..a9bc65bad --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiRuleAttribute.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The Validator attribute. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + internal class OpenApiRuleAttribute : Attribute + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs new file mode 100644 index 000000000..e12ebdf87 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiServerRules + { + /// + /// Validate the field is required. + /// + public static ValidationRule FieldIsRequired => + new ValidationRule( + (context, server) => + { + context.Push("url"); + if (String.IsNullOrEmpty(server.Url)) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "url", "server")); + context.AddError(error); + } + context.Pop(); + }); + + // add more rules + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs new file mode 100644 index 000000000..78c2c972d --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiTagRules.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiTagRules + { + /// + /// Validate the field is required. + /// + public static ValidationRule FieldIsRequired => + new ValidationRule( + (context, tag) => + { + context.Push("name"); + if (String.IsNullOrEmpty(tag.Name)) + { + ValidationError error = new ValidationError(ErrorReason.Required, context.PathString, + String.Format(SRResource.Validation_FieldIsRequired, "name", "tag")); + context.AddError(error); + } + context.Pop(); + }); + + // add more rules + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs b/src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs new file mode 100644 index 000000000..5f369fd9f --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; + +namespace Microsoft.OpenApi.Validations.Rules +{ + internal static class RuleHelpers + { + /// + /// Input string must be in the format of an email address + /// + /// The input string. + /// True if it's an email address. Otherwise False. + public static bool IsEmailAddress(this string input) + { + if (String.IsNullOrEmpty(input)) + { + return false; + } + + var splits = input.Split('@'); + if (splits.Length != 2) + { + return false; + } + + if (String.IsNullOrEmpty(splits[0]) || String.IsNullOrEmpty(splits[1])) + { + return false; + } + + // Add more rules. + + return true; + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/ValidationContext.cs b/src/Microsoft.OpenApi/Validations/ValidationContext.cs new file mode 100644 index 000000000..866e21e6e --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/ValidationContext.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Validations.Rules; + +namespace Microsoft.OpenApi.Validations +{ + /// + /// The validation context. + /// + public class ValidationContext + { + private readonly IList _errors = new List(); + + /// + /// Initializes the class. + /// + /// + public ValidationContext(ValidationRuleSet ruleSet) + { + RuleSet = ruleSet ?? throw Error.ArgumentNull(nameof(ruleSet)); + } + + /// + /// Gets the rule set. + /// + public ValidationRuleSet RuleSet { get; } + + /// + /// Gets the validation errors. + /// + public IEnumerable Errors + { + get + { + return _errors; + } + } + + /// + /// Register an error with the validation context. + /// + /// Error to register. + public void AddError(ValidationError error) + { + if (error == null) + { + throw Error.ArgumentNull(nameof(error)); + } + + _errors.Add(error); + } + + #region Visit Path + private readonly Stack _path = new Stack(); + + internal void Push(string segment) + { + this._path.Push(segment); + } + + internal void Pop() + { + this._path.Pop(); + } + + internal string PathString + { + get + { + return "#/" + String.Join("/", _path); + } + } + #endregion + } +} diff --git a/src/Microsoft.OpenApi/Validations/ValidationError.cs b/src/Microsoft.OpenApi/Validations/ValidationError.cs new file mode 100644 index 000000000..8ea695fa0 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/ValidationError.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.OpenApi.Validations +{ + /// + /// Error reason. + /// + public enum ErrorReason + { + /// + /// Field is required. + /// + Required, + + /// + /// Format error. + /// + Format, + } + + /// + /// The validation error class. + /// + public sealed class ValidationError + { + /// + /// Initializes the class. + /// + /// The error reason. + /// The visit path. + /// The error message. + public ValidationError(ErrorReason reason, string path, string message) + { + ErrorCode = reason; + ErrorPath = path; + ErrorMessage = message; + } + + /// + /// Gets the path of the error in the Open API in which it occurred. + /// + public string ErrorPath { get; private set; } + + /// + /// Gets an integer code representing the error. + /// + public ErrorReason ErrorCode { get; private set; } + + /// + /// Gets a human readable string describing the error. + /// + public string ErrorMessage { get; private set; } + + /// + /// Returns the whole error message. + /// + /// The error string. + public override string ToString() + { + return "ErrorCode: " + ErrorCode + ", " + ErrorPath + " | " + ErrorMessage; + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/ValidationRule.cs b/src/Microsoft.OpenApi/Validations/ValidationRule.cs new file mode 100644 index 000000000..beb3f519b --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/ValidationRule.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations +{ + /// + /// Class containing validation rule logic. + /// + public abstract class ValidationRule + { + /// + /// Element Type. + /// + internal abstract Type ElementType { get; } + + /// + /// Validate the object. + /// + /// The context. + /// The object item. + internal abstract void Evaluate(ValidationContext context, object item); + } + + /// + /// Class containing validation rule logic for . + /// + /// + public class ValidationRule : ValidationRule where T: IOpenApiElement + { + private readonly Action _validate; + + /// + /// Initializes a new instance of the class. + /// + /// Action to perform the validation. + public ValidationRule(Action validate) + { + _validate = validate ?? throw Error.ArgumentNull(nameof(validate)); + } + + internal override Type ElementType + { + get { return typeof(T); } + } + + internal override void Evaluate(ValidationContext context, object item) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (item == null) + { + return; + } + + if (!(item is T)) + { + throw Error.Argument(string.Format(SRResource.InputItemShouldBeType, typeof(T).FullName)); + } + + T typedItem = (T)item; + this._validate(context, typedItem); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs b/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs new file mode 100644 index 000000000..8cc9a78b0 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Linq; +using System.Reflection; +using System.Collections; +using System.Collections.Generic; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Validations.Rules; + +namespace Microsoft.OpenApi.Validations +{ + /// + /// The rule set of the validation. + /// + public sealed class ValidationRuleSet : IEnumerable + { + private IDictionary> _rules = new Dictionary>(); + + private static ValidationRuleSet _defaultRuleSet; + + /// + /// Gets the default validation rule sets. + /// + public static ValidationRuleSet DefaultRuleSet + { + get + { + if (_defaultRuleSet == null) + { + _defaultRuleSet = new Lazy(() => BuildDefaultRuleSet(), isThreadSafe: false).Value; + } + + return _defaultRuleSet; + } + } + + /// + /// Initializes a new instance of the class. + /// + public ValidationRuleSet() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Rules to be contained in this ruleset. + public ValidationRuleSet(IEnumerable rules) + { + if (rules != null) + { + foreach (ValidationRule rule in rules) + { + Add(rule); + } + } + } + + /// + /// Gets the rules in this rule set. + /// + public IEnumerable Rules + { + get + { + return _rules.Values.SelectMany(v => v); + } + } + + /// + /// Add the new rule into rule set. + /// + /// The rule. + public void Add(ValidationRule rule) + { + IList typeRules; + if (!_rules.TryGetValue(rule.ElementType, out typeRules)) + { + typeRules = new List(); + _rules[rule.ElementType] = typeRules; + } + + if (typeRules.Contains(rule)) + { + throw new OpenApiException(SRResource.Validation_RuleAddTwice); + } + + typeRules.Add(rule); + } + + /// + /// Get the enumerator. + /// + /// The enumerator. + public IEnumerator GetEnumerator() + { + foreach (List ruleList in _rules.Values) + { + foreach (var rule in ruleList) + { + yield return rule; + } + } + } + + /// + /// Get the enumerator. + /// + /// The enumerator. + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + private static ValidationRuleSet BuildDefaultRuleSet() + { + ValidationRuleSet ruleSet = new ValidationRuleSet(); + + IEnumerable allTypes = typeof(ValidationRuleSet).Assembly.GetTypes().Where(t => t.IsClass && t != typeof(object)); + Type validationRuleType = typeof(ValidationRule); + foreach (Type type in allTypes) + { + if (!type.GetCustomAttributes(typeof(OpenApiRuleAttribute), false).Any()) + { + continue; + } + + var properties = type.GetProperties(BindingFlags.Static | BindingFlags.Public); + foreach (var property in properties) + { + if (validationRuleType.IsAssignableFrom(property.PropertyType)) + { + var propertyValue = property.GetValue(null); // static property + ValidationRule rule = propertyValue as ValidationRule; + if (rule != null) + { + ruleSet.Add(rule); + } + } + } + } + + return ruleSet; + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs index af5ec8a05..34d5ae1d2 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; using FluentAssertions; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations; using Xunit; namespace Microsoft.OpenApi.Tests.Services @@ -17,7 +20,11 @@ public class OpenApiValidatorTests public void ResponseMustHaveADescription() { var openApiDocument = new OpenApiDocument(); - + openApiDocument.Info = new OpenApiInfo() + { + Title = "foo", + Version = "1.2.2" + }; openApiDocument.Paths.Add( "/test", new OpenApiPathItem @@ -38,11 +45,12 @@ public void ResponseMustHaveADescription() var walker = new OpenApiWalker(validator); walker.Walk(openApiDocument); - validator.Exceptions.ShouldBeEquivalentTo( - new List - { - new OpenApiException("Response must have a description") - }); + validator.Errors.ShouldBeEquivalentTo( + new List + { + new ValidationError(ErrorReason.Required, "#/description", + String.Format(SRResource.Validation_FieldIsRequired, "description", "response")) + }); } } } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs new file mode 100644 index 000000000..43aaefe57 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations.Rules; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiComponentsValidationTests + { + [Fact] + public void ValidateKeyMustMatchRegularExpressionInComponents() + { + // Arrange + const string key = "%@abc"; + + OpenApiComponents components = new OpenApiComponents() + { + Responses = new Dictionary + { + { key, new OpenApiResponse { Description = "any" } } + } + }; + + var errors = components.Validate(); + + // Act + bool result = !errors.Any(); + + + // Assert + Assert.False(result); + Assert.NotNull(errors); + ValidationError error = Assert.Single(errors); + Assert.Equal(String.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, "responses", OpenApiComponentsRules.KeyRegex.ToString()), + error.ErrorMessage); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiContactValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiContactValidationTests.cs new file mode 100644 index 000000000..80f4743a7 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiContactValidationTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiContactValidationTests + { + [Fact] + public void ValidateEmailFieldIsEmailAddressInContact() + { + // Arrange + const string testEmail = "support/example.com"; + + OpenApiContact contact = new OpenApiContact() + { + Email = testEmail + }; + + // Act + var errors = contact.Validate(); + bool result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + ValidationError error = Assert.Single(errors); + Assert.Equal(String.Format(SRResource.Validation_StringMustBeEmailAddress, testEmail), error.ErrorMessage); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiExternalDocsValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiExternalDocsValidationTests.cs new file mode 100644 index 000000000..6f350d407 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiExternalDocsValidationTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiExternalDocsValidationTests + { + [Fact] + public void ValidateUrlIsRequiredInExternalDocs() + { + // Arrange + OpenApiExternalDocs externalDocs = new OpenApiExternalDocs(); + + // Act + var errors = externalDocs.Validate(); + + // Assert + + bool result = !errors.Any(); + + Assert.False(result); + Assert.NotNull(errors); + ValidationError error = Assert.Single(errors); + Assert.Equal(String.Format(SRResource.Validation_FieldIsRequired, "url", "External Documentation"), error.ErrorMessage); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiInfoValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiInfoValidationTests.cs new file mode 100644 index 000000000..285d64be7 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiInfoValidationTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiInfoValidationTests + { + [Fact] + public void ValidateFieldIsRequiredInInfo() + { + // Arrange + string urlError = String.Format(SRResource.Validation_FieldIsRequired, "url", "info"); + string versionError = String.Format(SRResource.Validation_FieldIsRequired, "version", "info"); + IEnumerable errors; + OpenApiInfo info = new OpenApiInfo(); + + // Act + var validator = new OpenApiValidator(); + var walker = new OpenApiWalker(validator); + walker.Walk(info); + + + // Assert + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + + Assert.Equal(new[] { urlError, versionError }, errors.Select(e => e.ErrorMessage)); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiLicenseValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiLicenseValidationTests.cs new file mode 100644 index 000000000..ad542ba5f --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiLicenseValidationTests.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiLicenseValidationTests + { + [Fact] + public void ValidateFieldIsRequiredInLicense() + { + // Arrange + IEnumerable errors; + OpenApiLicense license = new OpenApiLicense(); + + // Act + var validator = new OpenApiValidator(); + var walker = new OpenApiWalker(validator); + walker.Walk(license); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + ValidationError error = Assert.Single(errors); + Assert.Equal(String.Format(SRResource.Validation_FieldIsRequired, "name", "license"), error.ErrorMessage); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiOAuthFlowValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiOAuthFlowValidationTests.cs new file mode 100644 index 000000000..3d38e60e8 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiOAuthFlowValidationTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiOAuthFlowValidationTests + { + [Fact] + public void ValidateFixedFieldsIsRequiredInResponse() + { + // Arrange + string authorizationUrlError = String.Format(SRResource.Validation_FieldIsRequired, "authorizationUrl", "OAuth Flow"); + string tokenUrlError = String.Format(SRResource.Validation_FieldIsRequired, "tokenUrl", "OAuth Flow"); + IEnumerable errors; + OpenApiOAuthFlow oAuthFlow = new OpenApiOAuthFlow(); + + // Act + var validator = new OpenApiValidator(); + var walker = new OpenApiWalker(validator); + walker.Walk(oAuthFlow); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + Assert.Equal(2, errors.Count()); + Assert.Equal(new[] { authorizationUrlError, tokenUrlError }, errors.Select(e => e.ErrorMessage)); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiResponseValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiResponseValidationTests.cs new file mode 100644 index 000000000..494bc0274 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiResponseValidationTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiResponseValidationTests + { + [Fact] + public void ValidateDescriptionIsRequiredInResponse() + { + // Arrange + IEnumerable errors; + OpenApiResponse response = new OpenApiResponse(); + + // Act + var validator = new OpenApiValidator(); + var walker = new OpenApiWalker(validator); + walker.Walk(response); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + ValidationError error = Assert.Single(errors); + Assert.Equal(String.Format(SRResource.Validation_FieldIsRequired, "description", "response"), error.ErrorMessage); + Assert.Equal(ErrorReason.Required, error.ErrorCode); + Assert.Equal("#/description", error.ErrorPath); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiServerValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiServerValidationTests.cs new file mode 100644 index 000000000..f3f5ece46 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiServerValidationTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiServerValidationTests + { + [Fact] + public void ValidateFieldIsRequiredInServer() + { + // Arrange + IEnumerable errors; + OpenApiServer server = new OpenApiServer(); + + // Act + var validator = new OpenApiValidator(); + validator.Visit(server); + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + ValidationError error = Assert.Single(errors); + Assert.Equal(String.Format(SRResource.Validation_FieldIsRequired, "url", "server"), error.ErrorMessage); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiTagValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiTagValidationTests.cs new file mode 100644 index 000000000..56696ffac --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiTagValidationTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiTagValidationTests + { + [Fact] + public void ValidateNameIsRequiredInTag() + { + // Arrange + IEnumerable errors; + OpenApiTag tag = new OpenApiTag(); + + // Act + var validator = new OpenApiValidator(); + validator.Visit(tag); + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + ValidationError error = Assert.Single(errors); + Assert.Equal(String.Format(SRResource.Validation_FieldIsRequired, "name", "tag"), error.ErrorMessage); + } + + [Fact] + public void ValidateExtensionNameStartsWithXDashInTag() + { + // Arrange + IEnumerable errors; + OpenApiTag tag = new OpenApiTag + { + Name = "tag" + }; + tag.Extensions.Add("tagExt", new OpenApiString("value")); + + // Act + var validator = new OpenApiValidator(); + validator.Visit(tag as IOpenApiExtensible); + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + Assert.False(result); + Assert.NotNull(errors); + ValidationError error = Assert.Single(errors); + Assert.Equal(String.Format(SRResource.Validation_ExtensionNameMustBeginWithXDash, "tagExt", "#/extensions"), error.ErrorMessage); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs new file mode 100644 index 000000000..9e4bfc80c --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Linq; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class ValidationRuleSetTests + { + [Fact] + public void DefaultRuleSetReturnsTheCorrectRules() + { + // Arrange + var ruleSet = new ValidationRuleSet(); + + // Act + var rules = ruleSet.Rules; + + // Assert + Assert.NotNull(rules); + Assert.Empty(rules); + } + + [Fact] + public void DefaultRuleSetPropertyReturnsTheCorrectRules() + { + // Arrange & Act + var ruleSet = ValidationRuleSet.DefaultRuleSet; + Assert.NotNull(ruleSet); // guard + + var rules = ruleSet.Rules; + + // Assert + Assert.NotNull(rules); + Assert.NotEmpty(rules); + Assert.Equal(13, rules.ToList().Count); // please update the number if you add new rule. + } + } +}