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());
+ }
+ }
+}