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/Validations/OpenApiElementValidator.cs b/src/Microsoft.OpenApi/Validations/OpenApiElementValidator.cs new file mode 100644 index 000000000..de38e15d5 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/OpenApiElementValidator.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Validations.Rules; + +namespace Microsoft.OpenApi.Validations +{ + /// + /// The public APIs to validate the Open API element. + /// + public static class OpenApiElementValidator + { + /// + /// Validate the Open API element. + /// + /// The Open API element type. + /// The Open API element. + /// True means no errors, otherwise with errors. + public static bool Validate(this T element) where T : IOpenApiElement + { + IEnumerable errors; + return element.Validate(out errors); + } + + /// + /// Validate the Open API element and output the whole validation errors. + /// + /// The Open API element type. + /// The Open API element. + /// The output errors. + /// True means no errors, otherwise with errors. + public static bool Validate(this T element, out IEnumerable errors) + where T : IOpenApiElement + { + ValidationRuleSet ruleSet = ValidationRuleSet.DefaultRuleSet; + return element.Validate(ruleSet, out errors); + } + + /// + /// Validate the Open API element with the given rule set and output the whole validation errors. + /// + /// The Open API element type. + /// The Open API element. + /// The input rule set. + /// The output errors. + /// True means no errors, otherwise with errors. + public static bool Validate(this T element, ValidationRuleSet ruleSet, out IEnumerable errors) + where T : IOpenApiElement + { + errors = null; + if (element == null) + { + return true; + } + + if (ruleSet == null) + { + ruleSet = ValidationRuleSet.DefaultRuleSet; + } + + ValidationContext context = new ValidationContext(ruleSet); + + context.Validate(element); + + errors = context.Errors; + + return !errors.Any(); + } + } +} 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/ValidationContextExtensions.cs b/src/Microsoft.OpenApi/Validations/ValidationContextExtensions.cs new file mode 100644 index 000000000..db6056592 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/ValidationContextExtensions.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Validations.Visitors; + +namespace Microsoft.OpenApi.Validations +{ + internal static class ValidationContextExtensions + { + /// + /// Validate the Open API element. + /// + /// The Open API element. + /// The validation cotext. + /// The Open API element. + public static void Validate(this ValidationContext context, T element) where T : IOpenApiElement + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (element != null) + { + IVisitor visitor = OpenApiVisitorSet.GetVisitor(typeof(T)); + if (visitor != null) + { + visitor.Visit(context, element); + } + } + } + + /// + /// Validate the collection of Open API element. + /// + /// The Open API element. + /// The validation cotext. + /// The collection of Open API element + public static void ValidateCollection(this ValidationContext context, IEnumerable collection) + where T : IOpenApiElement + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (collection != null) + { + foreach (var element in collection) + { + context.Validate(element); + } + } + } + + /// + /// Validate the map of Open API element. + /// + /// The Open API element. + /// The validation cotext. + /// The map of Open API element. + public static void ValidateMap(this ValidationContext context, IDictionary elements) + where T : IOpenApiElement + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (elements != null) + { + foreach (var element in elements) + { + context.Validate(element.Value); + } + } + } + } +} 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/src/Microsoft.OpenApi/Validations/Visitors/CallbackVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/CallbackVisitor.cs new file mode 100644 index 000000000..94e2f95ce --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/CallbackVisitor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class CallbackVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiCallback callback) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (callback == null) + { + throw Error.ArgumentNull(nameof(callback)); + } + + foreach (var pathItem in callback.PathItems) + { + context.Validate(pathItem.Value); + } + + base.Next(context, callback); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ComponentsVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ComponentsVisitor.cs new file mode 100644 index 000000000..7bd6c4e3b --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ComponentsVisitor.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ComponentsVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiComponents components) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (components == null) + { + throw Error.ArgumentNull(nameof(components)); + } + + context.ValidateMap(components.Schemas); + + context.ValidateMap(components.Responses); + + context.ValidateMap(components.Parameters); + + context.ValidateMap(components.Examples); + + context.ValidateMap(components.RequestBodies); + + context.ValidateMap(components.Headers); + + context.ValidateMap(components.SecuritySchemes); + + context.ValidateMap(components.Links); + + context.ValidateMap(components.Callbacks); + + base.Next(context, components); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ContactVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ContactVisitor.cs new file mode 100644 index 000000000..bc22aab7b --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ContactVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ContactVisitor : VisitorBase + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/DiscriminatorVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/DiscriminatorVisitor.cs new file mode 100644 index 000000000..24657ed94 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/DiscriminatorVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class DiscriminatorVisitor : VisitorBase + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/DocumentVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/DocumentVisitor.cs new file mode 100644 index 000000000..79f20be21 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/DocumentVisitor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Vistor for + /// + internal class DocumentVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiDocument document) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (document == null) + { + throw Error.ArgumentNull(nameof(document)); + } + + context.Validate(document.Info); + + context.ValidateCollection(document.Servers); + + context.Validate(document.Paths); + + context.Validate(document.Components); + + context.ValidateCollection(document.SecurityRequirements); + + context.ValidateCollection(document.Tags); + + context.Validate(document.ExternalDocs); + + base.Next(context, document); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/EncodingVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/EncodingVisitor.cs new file mode 100644 index 000000000..47e2f8337 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/EncodingVisitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class EncodingVisitor : VisitorBase + { + /// + /// Visit the children in . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiEncoding encoding) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (encoding == null) + { + throw Error.ArgumentNull(nameof(encoding)); + } + + context.ValidateMap(encoding.Headers); + + base.Next(context, encoding); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ExampleVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ExampleVisitor.cs new file mode 100644 index 000000000..4dbe97d89 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ExampleVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ExampleVisitor : VisitorBase + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ExternalDocsVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ExternalDocsVisitor.cs new file mode 100644 index 000000000..279fd9310 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ExternalDocsVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ExternalDocsVisitor : VisitorBase + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/HeaderVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/HeaderVisitor.cs new file mode 100644 index 000000000..ee0307aca --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/HeaderVisitor.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class HeaderVisitor : VisitorBase + { + /// + /// Visit the children in . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiHeader header) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (header == null) + { + throw Error.ArgumentNull(nameof(header)); + } + + context.Validate(header.Schema); + + context.ValidateCollection(header.Examples); + + context.ValidateMap(header.Content); + + base.Next(context, header); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/IVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/IVisitor.cs new file mode 100644 index 000000000..df61d1563 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/IVisitor.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// The interface for visitor. + /// + internal interface IVisitor + { + /// + /// Visit the object. + /// + /// The validation context. + /// The validation object. + void Visit(ValidationContext context, object item); + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/InfoVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/InfoVisitor.cs new file mode 100644 index 000000000..9c5b167f2 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/InfoVisitor.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class InfoVisitor : VisitorBase + { + /// + /// Visit the children in . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiInfo info) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (info == null) + { + throw Error.ArgumentNull(nameof(info)); + } + + context.Validate(info.Contact); + + context.Validate(info.License); + + base.Next(context, info); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/LicenseVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/LicenseVisitor.cs new file mode 100644 index 000000000..2d9867830 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/LicenseVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class LicenseVisitor : VisitorBase + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/LinkVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/LinkVisitor.cs new file mode 100644 index 000000000..3cead4c66 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/LinkVisitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class LinkVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiLink link) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (link == null) + { + throw Error.ArgumentNull(nameof(link)); + } + + context.Validate(link.Server); + + base.Next(context, link); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/MediaTypeVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/MediaTypeVisitor.cs new file mode 100644 index 000000000..5b035e221 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/MediaTypeVisitor.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class MediaTypeVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiMediaType mediaType) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (mediaType == null) + { + throw Error.ArgumentNull(nameof(mediaType)); + } + + context.Validate(mediaType.Schema); + context.ValidateMap(mediaType.Examples); + context.ValidateMap(mediaType.Encoding); + + base.Next(context, mediaType); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/OAuthFlowVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/OAuthFlowVisitor.cs new file mode 100644 index 000000000..f2faa565e --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/OAuthFlowVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class OAuthFlowVisitor : VisitorBase + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/OAuthFlowsVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/OAuthFlowsVisitor.cs new file mode 100644 index 000000000..cf84b4311 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/OAuthFlowsVisitor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class OAuthFlowsVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiOAuthFlows oAuthFlows) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (oAuthFlows == null) + { + throw Error.ArgumentNull(nameof(oAuthFlows)); + } + + context.Validate(oAuthFlows.Implicit); + context.Validate(oAuthFlows.Password); + context.Validate(oAuthFlows.ClientCredentials); + context.Validate(oAuthFlows.AuthorizationCode); + + base.Next(context, oAuthFlows); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/OpenApiVisitorSet.cs b/src/Microsoft.OpenApi/Validations/Visitors/OpenApiVisitorSet.cs new file mode 100644 index 000000000..105dc3d9e --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/OpenApiVisitorSet.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Interfaces; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Class to cache the . + /// + internal static class OpenApiVisitorSet + { + private static IDictionary _visitors; + + /// + /// Gets the visitors + /// + public static IDictionary Visitors + { + get + { + if (_visitors == null) + { + _visitors = new Lazy>(() => BuildVisitorSet(), isThreadSafe: false).Value; + } + + return _visitors; + } + } + /// + /// Get the element visitor. + /// + /// The element type. + /// The element visitor or null. + public static IVisitor GetVisitor(Type elementType) + { + IVisitor visitor; + if (Visitors.TryGetValue(elementType, out visitor)) + { + return visitor; + } + + throw new OpenApiException(String.Format(SRResource.UnknownVisitorType, elementType.FullName)); + } + + private static IDictionary BuildVisitorSet() + { + IDictionary visitors = new Dictionary(); + + IEnumerable allTypes = typeof(OpenApiVisitorSet).Assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && t != typeof(object)); + + Type visitorInterfaceType = typeof(IVisitor); + Type elementType = typeof(IOpenApiElement); + foreach (Type type in allTypes) + { + if (!visitorInterfaceType.IsAssignableFrom(type)) + { + continue; + } + + Type baseType = type.BaseType; + if (baseType == null || !baseType.IsGenericType || + baseType.GetGenericTypeDefinition() != typeof(VisitorBase<>)) + { + continue; + } + + Type validationType = baseType.GetGenericArguments()[0]; + if (!elementType.IsAssignableFrom(validationType)) + { + continue; + } + + object instance = Activator.CreateInstance(type); + IVisitor visitor = instance as IVisitor; + if (visitor != null) + { + visitors[validationType] = visitor; + } + } + + return visitors; + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/OperationVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/OperationVisitor.cs new file mode 100644 index 000000000..5095a740b --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/OperationVisitor.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class OperationVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiOperation operation) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (operation == null) + { + throw Error.ArgumentNull(nameof(operation)); + } + + context.ValidateCollection(operation.Tags); + context.Validate(operation.ExternalDocs); + context.ValidateCollection(operation.Parameters); + context.Validate(operation.RequestBody); + context.Validate(operation.Responses); + context.ValidateMap(operation.Callbacks); + context.ValidateCollection(operation.Security); + context.ValidateCollection(operation.Servers); + + base.Next(context, operation); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ParameterVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ParameterVisitor.cs new file mode 100644 index 000000000..f4e42fcd1 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ParameterVisitor.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ParameterVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiParameter parameter) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (parameter == null) + { + throw Error.ArgumentNull(nameof(parameter)); + } + + context.Validate(parameter.Schema); + context.ValidateCollection(parameter.Examples); + context.ValidateMap(parameter.Content); + + base.Next(context, parameter); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/PathItemVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/PathItemVisitor.cs new file mode 100644 index 000000000..06154374f --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/PathItemVisitor.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class PathItemVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiPathItem pathItem) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (pathItem == null) + { + throw Error.ArgumentNull(nameof(pathItem)); + } + + foreach (var operation in pathItem.Operations) + { + context.Validate(operation.Value); + } + + context.ValidateCollection(pathItem.Servers); + context.ValidateCollection(pathItem.Parameters); + + base.Next(context, pathItem); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/PathsVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/PathsVisitor.cs new file mode 100644 index 000000000..6aac857c5 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/PathsVisitor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class PathsVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiPaths paths) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (paths == null) + { + throw Error.ArgumentNull(nameof(paths)); + } + + context.ValidateMap(paths); + base.Next(context, paths); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/RequestBodyVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/RequestBodyVisitor.cs new file mode 100644 index 000000000..eea6cb2cb --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/RequestBodyVisitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class RequestBodyVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiRequestBody requestBody) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (requestBody == null) + { + throw Error.ArgumentNull(nameof(requestBody)); + } + + context.ValidateMap(requestBody.Content); + + base.Next(context, requestBody); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ResponseVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ResponseVisitor.cs new file mode 100644 index 000000000..2e653c836 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ResponseVisitor.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ResponseVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiResponse response) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (response == null) + { + throw Error.ArgumentNull(nameof(response)); + } + + context.ValidateMap(response.Headers); + context.ValidateMap(response.Content); + context.ValidateMap(response.Links); + + base.Next(context, response); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ResponsesVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ResponsesVisitor.cs new file mode 100644 index 000000000..c1c6461fc --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ResponsesVisitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ResponsesVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiResponses responses) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (responses == null) + { + throw Error.ArgumentNull(nameof(responses)); + } + + context.ValidateMap(responses); + + base.Next(context, responses); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/SchemaVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/SchemaVisitor.cs new file mode 100644 index 000000000..76556d23b --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/SchemaVisitor.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class SchemaVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiSchema schema) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (schema == null) + { + throw Error.ArgumentNull(nameof(schema)); + } + + context.ValidateCollection(schema.AllOf); + + context.ValidateCollection(schema.OneOf); + + context.ValidateCollection(schema.AnyOf); + + context.Validate(schema.Not); + + context.Validate(schema.Items); + + context.ValidateMap(schema.Properties); + + context.Validate(schema.AdditionalProperties); + + context.Validate(schema.Discriminator); + + context.Validate(schema.ExternalDocs); + + context.Validate(schema.Xml); + + base.Next(context, schema); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/SecurityRequirementVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/SecurityRequirementVisitor.cs new file mode 100644 index 000000000..23cbb1a34 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/SecurityRequirementVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class SecurityRequirementVisitor : VisitorBase + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/SecuritySchemeVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/SecuritySchemeVisitor.cs new file mode 100644 index 000000000..1e19210e0 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/SecuritySchemeVisitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class SecuritySchemeVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiSecurityScheme securityScheme) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (securityScheme == null) + { + throw Error.ArgumentNull(nameof(securityScheme)); + } + + context.Validate(securityScheme.Flows); + + base.Next(context, securityScheme); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ServerVariableVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ServerVariableVisitor.cs new file mode 100644 index 000000000..5033f274d --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ServerVariableVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ServerVariableVisitor : VisitorBase + { + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/ServerVisitor.cs b/src/Microsoft.OpenApi/Validations/Visitors/ServerVisitor.cs new file mode 100644 index 000000000..9068f1bfe --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/ServerVisitor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class ServerVisitor : VisitorBase + { + /// + /// Visit the children of the . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiServer server) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (server == null) + { + throw Error.ArgumentNull(nameof(server)); + } + + context.ValidateMap(server.Variables); + + base.Next(context, server); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/TagVistor.cs b/src/Microsoft.OpenApi/Validations/Visitors/TagVistor.cs new file mode 100644 index 000000000..5db0e4018 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/TagVistor.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class TagVisitor : VisitorBase + { + /// + /// Visit the children in . + /// + /// The validation context. + /// The . + protected override void Next(ValidationContext context, OpenApiTag tag) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (tag == null) + { + throw Error.ArgumentNull(nameof(tag)); + } + + context.Validate(tag.ExternalDocs); + + base.Next(context, tag); + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/VisitorBase.cs b/src/Microsoft.OpenApi/Validations/Visitors/VisitorBase.cs new file mode 100644 index 000000000..378fcfb62 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/VisitorBase.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Linq; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// The base visitor class for Open Api elements. + /// + internal abstract class VisitorBase : IVisitor where T : IOpenApiElement + { + /// + /// Visit the element itself. + /// + /// The validation context. + /// The element. + public void Visit(ValidationContext context, object item) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (item == null) + { + return; // uplevel should verify + } + + if (!(item is T)) + { + throw Error.Argument(string.Format(SRResource.InputItemShouldBeType, typeof(T).FullName)); + } + + var rules = context.RuleSet.Where(r => r.ElementType == typeof(T)); + foreach (var rule in rules) + { + rule.Evaluate(context, item); + } + + // verify its extension if the input item is an extensible element. + IOpenApiExtensible extensible = item as IOpenApiExtensible; + if (extensible != null) + { + rules = context.RuleSet.Where(r => r.ElementType == typeof(IOpenApiExtensible)); + foreach (var rule in rules) + { + rule.Evaluate(context, extensible); + } + } + + T typedItem = (T)item; + Next(context, typedItem); + } + + /// + /// Visit the children. + /// + /// The validation context. + /// The element. + protected virtual void Next(ValidationContext context, T element) + { + } + } +} diff --git a/src/Microsoft.OpenApi/Validations/Visitors/XmlVistor.cs b/src/Microsoft.OpenApi/Validations/Visitors/XmlVistor.cs new file mode 100644 index 000000000..7a11486ac --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Visitors/XmlVistor.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Visitors +{ + /// + /// Visit . + /// + internal class XmlVisitor : VisitorBase + { + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs new file mode 100644 index 000000000..872f4a36a --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Validations.Rules; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiComponentsValidationTests + { + [Fact] + public void ValidateKeyMustMatchRegularExpressionInComponents() + { + // Arrange + const string key = "%@abc"; + IEnumerable errors; + OpenApiComponents components = new OpenApiComponents() + { + Responses = new Dictionary + { + { key, new OpenApiResponse { Description = "any" } } + } + }; + + // Act + bool result = components.Validate(out errors); + + // 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..94cbb6920 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiContactValidationTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiContactValidationTests + { + [Fact] + public void ValidateEmailFieldIsEmailAddressInContact() + { + // Arrange + const string testEmail = "support/example.com"; + IEnumerable errors; + OpenApiContact Contact = new OpenApiContact() + { + Email = testEmail + }; + + // Act + bool result = Contact.Validate(out errors); + + // 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..5989d7448 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiExternalDocsValidationTests.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.Models; +using Microsoft.OpenApi.Properties; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiExternalDocsValidationTests + { + [Fact] + public void ValidateUrlIsRequiredInExternalDocs() + { + // Arrange + IEnumerable errors; + OpenApiExternalDocs externalDocs = new OpenApiExternalDocs(); + + // Act + bool result = externalDocs.Validate(out errors); + + // Assert + 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..4945b84ab --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiInfoValidationTests.cs @@ -0,0 +1,34 @@ +// 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 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 + bool result = info.Validate(out errors); + + // 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..fb6fd9f35 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiLicenseValidationTests.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.Models; +using Microsoft.OpenApi.Properties; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiLicenseValidationTests + { + [Fact] + public void ValidateFieldIsRequiredInLicense() + { + // Arrange + IEnumerable errors; + OpenApiLicense license = new OpenApiLicense(); + + // Act + bool result = license.Validate(out errors); + + // 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..776ccbd45 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiOAuthFlowValidationTests.cs @@ -0,0 +1,34 @@ +// 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 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 + bool result = oAuthFlow.Validate(out errors); + + // 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..eb0a339af --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiResponseValidationTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiResponseValidationTests + { + [Fact] + public void ValidateDescriptionIsRequiredInResponse() + { + // Arrange + IEnumerable errors; + OpenApiResponse response = new OpenApiResponse(); + + // Act + bool result = response.Validate(out errors); + + // 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..bbbb05588 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiServerValidationTests.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.Models; +using Microsoft.OpenApi.Properties; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiServerValidationTests + { + [Fact] + public void ValidateFieldIsRequiredInServer() + { + // Arrange + IEnumerable errors; + OpenApiServer server = new OpenApiServer(); + + // Act + bool result = server.Validate(out errors); + + // 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..e34c125f1 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiTagValidationTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiTagValidationTests + { + [Fact] + public void ValidateNameIsRequiredInTag() + { + // Arrange + IEnumerable errors; + OpenApiTag tag = new OpenApiTag(); + + // Act + bool result = tag.Validate(out errors); + + // 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 + bool result = tag.Validate(out errors); + + // 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. + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/Visitors/OpenApiVisitorSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/Visitors/OpenApiVisitorSetTests.cs new file mode 100644 index 000000000..81ce4b451 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/Visitors/OpenApiVisitorSetTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Models; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Visitors.Tests +{ + public class OpenApiVisitorSetTests + { + [Fact] + public void VisitorsPropertyReturnsTheCorrectVisitorList() + { + for (int i = 0; i < 5; i++) // 5 is just a magic number + { + // Arrange & Act + var visitors = OpenApiVisitorSet.Visitors; + + // Assert + Assert.NotNull(visitors); + Assert.NotEmpty(visitors); + Assert.Equal(29, visitors.Count); // Now, we have 29 DOM classes. + } + } + + [Fact] + public void GetVisitorThrowsUnknowElementType() + { + // Arrange & Act + Action test = () => OpenApiVisitorSet.GetVisitor(typeof(OpenApiVisitorSetTests)); + + // Assert + var exception = Assert.Throws(test); + Assert.Equal("Can not find visitor type registered for type 'Microsoft.OpenApi.Validations.Visitors.Tests.OpenApiVisitorSetTests'.", + exception.Message); + } + + [Theory] + [InlineData(typeof(OpenApiCallback), typeof(CallbackVisitor))] + [InlineData(typeof(OpenApiComponents), typeof(ComponentsVisitor))] + [InlineData(typeof(OpenApiContact), typeof(ContactVisitor))] + [InlineData(typeof(OpenApiDiscriminator), typeof(DiscriminatorVisitor))] + [InlineData(typeof(OpenApiDocument), typeof(DocumentVisitor))] + [InlineData(typeof(OpenApiEncoding), typeof(EncodingVisitor))] + [InlineData(typeof(OpenApiExample), typeof(ExampleVisitor))] + [InlineData(typeof(OpenApiExternalDocs), typeof(ExternalDocsVisitor))] + [InlineData(typeof(OpenApiHeader), typeof(HeaderVisitor))] + [InlineData(typeof(OpenApiInfo), typeof(InfoVisitor))] + [InlineData(typeof(OpenApiLicense), typeof(LicenseVisitor))] + [InlineData(typeof(OpenApiLink), typeof(LinkVisitor))] + [InlineData(typeof(OpenApiMediaType), typeof(MediaTypeVisitor))] + [InlineData(typeof(OpenApiOAuthFlows), typeof(OAuthFlowsVisitor))] + [InlineData(typeof(OpenApiOAuthFlow), typeof(OAuthFlowVisitor))] + [InlineData(typeof(OpenApiOperation), typeof(OperationVisitor))] + [InlineData(typeof(OpenApiParameter), typeof(ParameterVisitor))] + [InlineData(typeof(OpenApiPathItem), typeof(PathItemVisitor))] + [InlineData(typeof(OpenApiPaths), typeof(PathsVisitor))] + [InlineData(typeof(OpenApiRequestBody), typeof(RequestBodyVisitor))] + [InlineData(typeof(OpenApiResponses), typeof(ResponsesVisitor))] + [InlineData(typeof(OpenApiResponse), typeof(ResponseVisitor))] + [InlineData(typeof(OpenApiSchema), typeof(SchemaVisitor))] + [InlineData(typeof(OpenApiSecurityRequirement), typeof(SecurityRequirementVisitor))] + [InlineData(typeof(OpenApiSecurityScheme), typeof(SecuritySchemeVisitor))] + [InlineData(typeof(OpenApiServerVariable), typeof(ServerVariableVisitor))] + [InlineData(typeof(OpenApiServer), typeof(ServerVisitor))] + [InlineData(typeof(OpenApiTag), typeof(TagVisitor))] + [InlineData(typeof(OpenApiXml), typeof(XmlVisitor))] + public void GetVisitorReturnsTheCorrectVisitor(Type elementType, Type visitorType) + { + // Arrange & Act + var visitor = OpenApiVisitorSet.GetVisitor(elementType); + + // Assert + Assert.NotNull(visitor); + Assert.Same(visitorType, visitor.GetType()); + } + } +}