From f533ad1c36265d69aeeef8581543cafe05730e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=87=E9=9B=85=E8=99=8E?= Date: Sat, 14 Oct 2023 21:51:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=86=85=E7=BD=AE=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E5=99=A8=E5=AF=B9NSwag=E7=9A=84=E6=94=AF=E6=8C=81=20#?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Biwen.QuickApi.DemoWeb/Apis/HelloApi.cs | 2 +- .../AppExtentions.cs | 5 + Biwen.QuickApi/Swagger/NSwagExtensions.cs | 1 + .../QuickApiValidationSchemaProcessor.cs | 332 ++++++++++++++++++ .../Extensions/ReflectionExtensions.cs | 81 +++++ .../Extensions/StringExtensions.cs | 134 +++++++ .../Extensions/ValidationExtensions.cs | 135 +++++++ .../FluentValidationRule.cs | 50 +++ .../ValidationProcessor/RuleContext.cs | 43 +++ .../Swagger/ValidationProcessor/readme.md | 1 + 10 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 Biwen.QuickApi/Swagger/QuickApiValidationSchemaProcessor.cs create mode 100644 Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/ReflectionExtensions.cs create mode 100644 Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/StringExtensions.cs create mode 100644 Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/ValidationExtensions.cs create mode 100644 Biwen.QuickApi/Swagger/ValidationProcessor/FluentValidationRule.cs create mode 100644 Biwen.QuickApi/Swagger/ValidationProcessor/RuleContext.cs create mode 100644 Biwen.QuickApi/Swagger/ValidationProcessor/readme.md diff --git a/Biwen.QuickApi.DemoWeb/Apis/HelloApi.cs b/Biwen.QuickApi.DemoWeb/Apis/HelloApi.cs index 42d3c669..1a0d3295 100644 --- a/Biwen.QuickApi.DemoWeb/Apis/HelloApi.cs +++ b/Biwen.QuickApi.DemoWeb/Apis/HelloApi.cs @@ -77,7 +77,7 @@ public class FromBodyRequest : BaseRequestFromBody public FromBodyRequest() { - RuleFor(x => x.Id).InclusiveBetween(1, 100);//必须1~100 + RuleFor(x => x.Id).NotNull().InclusiveBetween(1, 100);//必须1~100 } } diff --git a/Biwen.QuickApi.SourceGenerator.TestConsole/AppExtentions.cs b/Biwen.QuickApi.SourceGenerator.TestConsole/AppExtentions.cs index 662840e6..9847702b 100644 --- a/Biwen.QuickApi.SourceGenerator.TestConsole/AppExtentions.cs +++ b/Biwen.QuickApi.SourceGenerator.TestConsole/AppExtentions.cs @@ -9,6 +9,11 @@ using Biwen.QuickApi.SourceGenerator.TestConsole; using Microsoft.OpenApi.Models; + + +//用于测试生成器的代码 + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1050:在命名空间中声明类型", Justification = "<挂起>")] public static partial class AppExtentions { diff --git a/Biwen.QuickApi/Swagger/NSwagExtensions.cs b/Biwen.QuickApi/Swagger/NSwagExtensions.cs index 441a07c6..76d62243 100644 --- a/Biwen.QuickApi/Swagger/NSwagExtensions.cs +++ b/Biwen.QuickApi/Swagger/NSwagExtensions.cs @@ -30,6 +30,7 @@ public static IServiceCollection AddQuickApiDocument( { settings.OperationProcessors.Add(new QuickApiOperationProcessor()); settings.SchemaProcessors.Add(new QuickApiSchemaProcessor()); + settings.SchemaProcessors.Add(new QuickApiValidationSchemaProcessor()); if (securityOptions?.EnlableSecurityProcessor is true) { settings.OperationProcessors.Add(new QuickApiOperationSecurityProcessor(securityOptions)); diff --git a/Biwen.QuickApi/Swagger/QuickApiValidationSchemaProcessor.cs b/Biwen.QuickApi/Swagger/QuickApiValidationSchemaProcessor.cs new file mode 100644 index 00000000..6bc88dd3 --- /dev/null +++ b/Biwen.QuickApi/Swagger/QuickApiValidationSchemaProcessor.cs @@ -0,0 +1,332 @@ +using Biwen.QuickApi.Swagger.ValidationProcessor.Extensions; +using Biwen.QuickApi.Swagger.ValidationProcessor; +using FluentValidation.Internal; +using FluentValidation.Validators; +using NJsonSchema.Generation; +using NJsonSchema; +using System.Collections.ObjectModel; + +namespace Biwen.QuickApi.Swagger +{ + internal sealed class QuickApiValidationSchemaProcessor : ISchemaProcessor + { + readonly FluentValidationRule[] _rules; + readonly Dictionary _childAdaptorValidators = new(); + + static readonly Type _iReqValidatorType = typeof(IReqValidator<>); + + public QuickApiValidationSchemaProcessor() + { + _rules = CreateDefaultRules(); + } + public void Process(SchemaProcessorContext context) + { + var tRequest = context.ContextualType; + if (tRequest == null) + { + return; + } + + if (tRequest?.Type.IsClass is true && tRequest?.Type.IsPublic is true && tRequest?.Type != typeof(string)) + { + if (tRequest?.Type.GetProperties().Length == 0) + { + return; + } + if (tRequest?.Type.IsAbstract is true) + { + return; + } + if (tRequest?.Type.GetInterface(_iReqValidatorType.Name) is not null) + { + try + { + //dynamic + var validator = (dynamic)Activator.CreateInstance(tRequest.Type)!; + ApplyValidator(context.Schema, (IValidator)validator.RealValidator, string.Empty); + } + catch + { + //todo: + } + } + } + } + + void ApplyValidator(JsonSchema schema, IValidator validator, string propertyPrefix) + { + // Create dict of rules for this validator + var rulesDict = validator.GetDictionaryOfRules(); + ApplyRulesToSchema(schema, rulesDict, propertyPrefix); + ApplyRulesFromIncludedValidators(schema, validator); + } + + void ApplyRulesToSchema(JsonSchema? schema, + ReadOnlyDictionary> rulesDict, + string propertyPrefix) + { + if (schema is null) + return; + + // Add properties from current schema/class + if (schema.ActualProperties != null) + { + foreach (var schemaProperty in schema.ActualProperties.Keys) + TryApplyValidation(schema, rulesDict, schemaProperty, propertyPrefix); + } + + // Add properties from base class + ApplyRulesToSchema(schema.InheritedSchema, rulesDict, propertyPrefix); + } + + void ApplyRulesFromIncludedValidators(JsonSchema schema, IValidator validator) + { + if (validator is not IEnumerable rules) return; + + // Note: IValidatorDescriptor doesn't return IncludeRules so we need to get validators manually. + var childAdapters = rules + .Where(rule => rule.HasNoCondition() && rule is IIncludeRule) + .SelectMany(includeRule => includeRule.Components.Select(c => c.Validator)) + .Where(x => x.GetType().IsGenericType && x.GetType().GetGenericTypeDefinition() == typeof(ChildValidatorAdaptor<,>)) + .ToList(); + + foreach (var adapter in childAdapters) + { + var adapterMethod = adapter.GetType().GetMethod("GetValidator"); + if (adapterMethod == null) continue; + + // Create validation context of generic type + var validationContext = Activator.CreateInstance( + adapterMethod.GetParameters().First().ParameterType, new object[] { null! } + ); + + if (adapterMethod.Invoke(adapter, new[] { validationContext, null! }) is not IValidator includeValidator) + { + break; + } + + ApplyRulesToSchema(schema, includeValidator.GetDictionaryOfRules(), string.Empty); + ApplyRulesFromIncludedValidators(schema, includeValidator); + } + } + + void TryApplyValidation(JsonSchema schema, + ReadOnlyDictionary> rulesDict, + string propertyName, + string parameterPrefix) + { + // Build the full propertyname with composition route: request.child.property + var fullPropertyName = $"{parameterPrefix}{propertyName}"; + + // Try get a list of valid rules that matches this property name + if (rulesDict.TryGetValue(fullPropertyName, out var validationRules)) + { + // Go through each rule and apply it to the schema + foreach (var validationRule in validationRules) + ApplyValidationRule(schema, validationRule, propertyName); + } + + // If the property is a child object, recursively apply validation to it adding prefix as we go down one level + var property = schema.ActualProperties[propertyName]; + var propertySchema = property.ActualSchema; + if (propertySchema.ActualProperties is not null && propertySchema.ActualProperties.Count > 0 && propertySchema != schema) + ApplyRulesToSchema(propertySchema, rulesDict, $"{fullPropertyName}."); + } + + void ApplyValidationRule(JsonSchema schema, IValidationRule validationRule, string propertyName) + { + foreach (var ruleComponent in validationRule.Components) + { + var propertyValidator = ruleComponent.Validator; + + // 1. If the propertyValidator is a ChildValidatorAdaptor we need to get the underlying validator + // i.e. for RuleFor().SetValidator() or RuleForEach().SetValidator() + if (propertyValidator.Name == "ChildValidatorAdaptor") + { + // Get underlying validator using reflection + var validatorTypeObj = propertyValidator.GetType() + .GetProperty("ValidatorType") + ?.GetValue(propertyValidator); + // Check if something went wrong + if (validatorTypeObj is not Type validatorType) + throw new InvalidOperationException("ChildValidatorAdaptor.ValidatorType is null"); + + // Retrieve or create an instance of the validator + if (!_childAdaptorValidators.TryGetValue(validatorType.FullName!, out var childValidator)) + childValidator = _childAdaptorValidators[validatorType.FullName!] = (IValidator)Activator.CreateInstance(validatorType)!; + + // Apply the validator to the schema. Again, recursively + var childSchema = schema.ActualProperties[propertyName].ActualSchema; + // Check if it is an array (RuleForEach()). In this case we need to apply validator to an Item Schema + childSchema = childSchema.Type == JsonObjectType.Array ? childSchema.Item.ActualSchema : childSchema; + ApplyValidator(childSchema, childValidator, string.Empty); + + continue; + } + + // 2. Normal property validator processing + foreach (var rule in _rules) + { + if (!rule.Matches(propertyValidator)) + continue; + + try + { + rule.Apply(new RuleContext(schema, propertyName, propertyValidator)); + } + catch { } + } + } + } + + static FluentValidationRule[] CreateDefaultRules() => new[] + { + new FluentValidationRule("Required") + { + Matches = propertyValidator => propertyValidator is INotNullValidator or INotEmptyValidator, + Apply = context => + { + var schema = context.Schema; + if (!schema.RequiredProperties.Contains(context.PropertyKey)) + schema.RequiredProperties.Add(context.PropertyKey); + } + }, + new FluentValidationRule("NotNull") + { + Matches = propertyValidator => propertyValidator is INotNullValidator, + Apply = context => + { + var schema = context.Schema; + var properties = schema.ActualProperties; + properties[context.PropertyKey].IsNullableRaw = false; + if (properties[context.PropertyKey].Type.HasFlag(JsonObjectType.Null)) + properties[context.PropertyKey].Type &= ~JsonObjectType.Null; // Remove nullable + var oneOfsWithReference = properties[context.PropertyKey].OneOf + .Where(x => x.Reference != null) + .ToList(); + if (oneOfsWithReference.Count == 1) + { + // Set the Reference directly instead and clear the OneOf collection + properties[context.PropertyKey].Reference = oneOfsWithReference.Single(); + properties[context.PropertyKey].OneOf.Clear(); + } + } + }, + new FluentValidationRule("NotEmpty") + { + Matches = propertyValidator => propertyValidator is INotEmptyValidator, + Apply = context => + { + var schema = context.Schema; + var properties = schema.ActualProperties; + properties[context.PropertyKey].IsNullableRaw = false; + if (properties[context.PropertyKey].Type.HasFlag(JsonObjectType.Null)) + properties[context.PropertyKey].Type &= ~JsonObjectType.Null; // Remove nullable + var oneOfsWithReference = properties[context.PropertyKey].OneOf + .Where(x => x.Reference != null) + .ToList(); + if (oneOfsWithReference.Count == 1) + { + // Set the Reference directly instead and clear the OneOf collection + properties[context.PropertyKey].Reference = oneOfsWithReference.Single(); + properties[context.PropertyKey].OneOf.Clear(); + } + properties[context.PropertyKey].MinLength = 1; + } + }, + new FluentValidationRule("Length") + { + Matches = propertyValidator => propertyValidator is ILengthValidator, + Apply = context => + { + var schema = context.Schema; + var properties = schema.ActualProperties; + var lengthValidator = (ILengthValidator)context.PropertyValidator; + if (lengthValidator.Max > 0) + properties[context.PropertyKey].MaxLength = lengthValidator.Max; + if (lengthValidator.GetType() == typeof(MinimumLengthValidator<>) || + lengthValidator.GetType() == typeof(ExactLengthValidator<>) || + properties[context.PropertyKey].MinLength == null) + { + properties[context.PropertyKey].MinLength = lengthValidator.Min; + } + } + }, + new FluentValidationRule("Pattern") + { + Matches = propertyValidator => propertyValidator is IRegularExpressionValidator, + Apply = context => + { + var regularExpressionValidator = (IRegularExpressionValidator)context.PropertyValidator; + var schema = context.Schema; + var properties = schema.ActualProperties; + properties[context.PropertyKey].Pattern = regularExpressionValidator.Expression; + } + }, + new FluentValidationRule("Comparison") + { + Matches = propertyValidator => propertyValidator is IComparisonValidator, + Apply = context => + { + var comparisonValidator = (IComparisonValidator)context.PropertyValidator; + if (comparisonValidator.ValueToCompare.IsNumeric()) + { + var valueToCompare = Convert.ToDecimal(comparisonValidator.ValueToCompare); + var schema = context.Schema; + var properties = schema.ActualProperties; + var schemaProperty = properties[context.PropertyKey]; + if (comparisonValidator.Comparison == Comparison.GreaterThanOrEqual) + { + schemaProperty.Minimum = valueToCompare; + } + else if (comparisonValidator.Comparison == Comparison.GreaterThan) + { + schemaProperty.Minimum = valueToCompare; + schemaProperty.IsExclusiveMinimum = true; + } + else if (comparisonValidator.Comparison == Comparison.LessThanOrEqual) { schemaProperty.Maximum = valueToCompare; } else if (comparisonValidator.Comparison == Comparison.LessThan) + { + schemaProperty.Maximum = valueToCompare; + schemaProperty.IsExclusiveMaximum = true; + } + } + } + }, + new FluentValidationRule("Between") + { + Matches = propertyValidator => propertyValidator is IBetweenValidator, + Apply = context => + { + var betweenValidator = (IBetweenValidator)context.PropertyValidator; + var schema = context.Schema; + var properties = schema.ActualProperties; + var schemaProperty = properties[context.PropertyKey]; + if (betweenValidator.From.IsNumeric()) + { + if (betweenValidator.GetType().IsSubClassOfGeneric(typeof(ExclusiveBetweenValidator<,>))) + schemaProperty.ExclusiveMinimum = Convert.ToDecimal(betweenValidator.From); + else + schemaProperty.Minimum = Convert.ToDecimal(betweenValidator.From); + } + if (betweenValidator.To.IsNumeric()) + { + if (betweenValidator.GetType().IsSubClassOfGeneric(typeof(ExclusiveBetweenValidator<,>))) + schemaProperty.ExclusiveMaximum = Convert.ToDecimal(betweenValidator.To); + else + schemaProperty.Maximum = Convert.ToDecimal(betweenValidator.To); + } + } + }, + new FluentValidationRule("AspNetCoreCompatibleEmail") + { + Matches = propertyValidator => propertyValidator.GetType().IsSubClassOfGeneric(typeof(AspNetCoreCompatibleEmailValidator<>)), + Apply = context => + { + var schema = context.Schema; + var properties = schema.ActualProperties; + properties[context.PropertyKey].Pattern = "^[^@]+@[^@]+$"; // [^@] All chars except @ + } + }, + }; + } +} \ No newline at end of file diff --git a/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/ReflectionExtensions.cs b/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/ReflectionExtensions.cs new file mode 100644 index 00000000..cb011e3f --- /dev/null +++ b/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/ReflectionExtensions.cs @@ -0,0 +1,81 @@ +using System.Reflection; + +namespace Biwen.QuickApi.Swagger.ValidationProcessor.Extensions; + +static class ReflectionExtension +{ + internal static bool IsSubClassOfGeneric(this Type? child, Type parent) + { + if (child == parent) + return false; + + if (child?.IsSubclassOf(parent) is true) + return true; + + var parameters = parent.GetGenericArguments(); + + var isParameterLessGeneric = !(parameters is { Length: > 0 } && + (parameters[0].Attributes & TypeAttributes.BeforeFieldInit) == TypeAttributes.BeforeFieldInit); + + while (child != null && child != typeof(object)) + { + var cur = GetFullTypeDefinition(child); + + if (parent == cur || (isParameterLessGeneric && cur.GetInterfaces() + .Select(GetFullTypeDefinition) + .Contains(GetFullTypeDefinition(parent)))) + { + return true; + } + + if (!isParameterLessGeneric) + { + if (GetFullTypeDefinition(parent) == cur && !cur.IsInterface) + { + if (VerifyGenericArguments(GetFullTypeDefinition(parent), cur) && VerifyGenericArguments(parent, child)) + return true; + } + else + { + if (child.GetInterfaces() + .Where(i => GetFullTypeDefinition(parent) == GetFullTypeDefinition(i)) + .Any(item => VerifyGenericArguments(parent, item))) + { + return true; + } + } + } + + child = child.BaseType; + } + + return false; + } + + static Type GetFullTypeDefinition(Type type) + => type.IsGenericType ? type.GetGenericTypeDefinition() : type; + + static bool VerifyGenericArguments(Type parent, Type child) + { + var childArguments = child.GetGenericArguments(); + var parentArguments = parent.GetGenericArguments(); + + if (childArguments.Length != parentArguments.Length) + return true; + + for (var i = 0; i < childArguments.Length; i++) + { + if (childArguments[i].Assembly == parentArguments[i].Assembly && + childArguments[i].Name == parentArguments[i].Name && + childArguments[i].Namespace == parentArguments[i].Namespace) + { + continue; + } + + if (!childArguments[i].IsSubclassOf(parentArguments[i])) + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/StringExtensions.cs b/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/StringExtensions.cs new file mode 100644 index 00000000..1be06f44 --- /dev/null +++ b/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/StringExtensions.cs @@ -0,0 +1,134 @@ +// Original: https://github.com/zymlabs/nswag-fluentvalidation +// MIT License +// Copyright (c) 2019 Zym Labs LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace Biwen.QuickApi.Swagger.ValidationProcessor.Extensions; + +static class StringExtensions +{ + /// + /// Converts string to lowerCamelCase. + /// + /// Input string. + /// lowerCamelCase string. + internal static string? ToLowerCamelCase(this string? inputString) + { + return inputString switch + { + null => null, + "" => string.Empty, + _ => char.IsLower(inputString[0]) ? inputString : inputString[..1].ToLower() + inputString[1..], + }; + } + + /// + /// Returns string equality only by symbols ignore case. + /// It can be used for comparing camelCase, PascalCase, snake_case, kebab-case identifiers. + /// + /// Left string to compare. + /// Right string to compare. + /// true if input strings are equals in terms of identifier formatting. + internal static bool EqualsIgnoreAll(this string left, string right) => IgnoreAllStringComparer.Instance.Equals(left, right); +} + +/// +/// Returns string equality only by symbols ignore case. +/// It can be used for comparing camelCase, PascalCase, snake_case, kebab-case identifiers. +/// +class IgnoreAllStringComparer : StringComparer +{ + /// + /// Instance of StringComparer + /// + public static readonly StringComparer Instance = new IgnoreAllStringComparer(); + + /// + public override int Compare(string? left, string? right) + { + var leftIndex = 0; + var rightIndex = 0; + int compare; + do + { + GetNextSymbol(left, ref leftIndex, out var leftSymbol); + GetNextSymbol(right, ref rightIndex, out var rightSymbol); + + compare = leftSymbol.CompareTo(rightSymbol); + } + while (compare == 0 && leftIndex >= 0 && rightIndex >= 0); + + return compare; + } + + /// + public override bool Equals(string? left, string? right) + { + if (left == null || right == null) + return false; + + var leftIndex = 0; + var rightIndex = 0; + bool equals; + + while (true) + { + var hasLeftSymbol = GetNextSymbol(left, ref leftIndex, out var leftSymbol); + var hasRightSymbol = GetNextSymbol(right, ref rightIndex, out var rightSymbol); + + equals = leftSymbol == rightSymbol; + if (!equals || !hasLeftSymbol || !hasRightSymbol) + break; + } + + return equals; + } + + /// + public override int GetHashCode(string obj) + { + unchecked + { + var index = 0; + var hash = 0; + while (GetNextSymbol(obj, ref index, out var symbol)) + hash = (31 * hash) + char.ToUpperInvariant(symbol).GetHashCode(); + + return hash; + } + } + + internal static bool GetNextSymbol(string? value, ref int startIndex, out char symbol) + { + while (startIndex >= 0 && startIndex < value?.Length) + { + var current = value[startIndex++]; + if (char.IsLetterOrDigit(current)) + { + symbol = char.ToUpperInvariant(current); + return true; + } + } + + startIndex = -1; + symbol = char.MinValue; + return false; + } +} \ No newline at end of file diff --git a/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/ValidationExtensions.cs b/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/ValidationExtensions.cs new file mode 100644 index 00000000..982b426c --- /dev/null +++ b/Biwen.QuickApi/Swagger/ValidationProcessor/Extensions/ValidationExtensions.cs @@ -0,0 +1,135 @@ +// Original: https://github.com/zymlabs/nswag-fluentvalidation +// MIT License +// Copyright (c) 2019 Zym Labs LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using FluentValidation; +using System.Collections.ObjectModel; +using System.Text.Json; + +namespace Biwen.QuickApi.Swagger.ValidationProcessor.Extensions; + +/// +/// Extensions for some swagger specific work. +/// +static class ValidationExtensions +{ + /// + /// Is supported swagger numeric type. + /// + public static bool IsNumeric(this object value) => value is int or long or float or double or decimal; + + /// + /// Returns not null enumeration. + /// + public static IEnumerable NotNull(this IEnumerable? collection) => collection ?? Array.Empty(); + + /// + /// Creates a dictionary with the validation rules. + /// Keys are the property names of the rules, converted to the schema casing by using the selected JsonNamingPolicy + /// + /// + public static ReadOnlyDictionary> GetDictionaryOfRules(this IValidator validator) + { + // Dictionary that will hold the rules with a key of the property name with casing that matches the selected JsonNamingPolicy + var rulesDict = new Dictionary>(); + + if (validator is IEnumerable rules) + { + foreach (var rule in rules.GetPropertyRules()) + { + var propertyNameRaw = rule.ValidationRule.PropertyName; + //default JsonNamingPolicy.CamelCase + var propertyNameWithSchemaCasing = propertyNameRaw.ConvertToSchemaCasing(JsonNamingPolicy.CamelCase); + if (rulesDict.TryGetValue(propertyNameWithSchemaCasing, out var propertyRules)) + propertyRules.Add(rule.ValidationRule); + else + rulesDict.Add(propertyNameWithSchemaCasing, new List { rule.ValidationRule }); + } + } + + return new ReadOnlyDictionary>(rulesDict); + } + + /// + /// Converts a property name (route of) to the schema casing by using the selected JsonNamingPolicy + /// + /// The property name can have points as it defines the object composition hierarchy + /// + /// + public static string ConvertToSchemaCasing(this string propertyName, JsonNamingPolicy? namingPolicy) + { + if (namingPolicy is null) + return propertyName; + + var segments = propertyName.Split('.'); + for (var i = 0; i < segments.Length; i++) + segments[i] = namingPolicy.ConvertName(segments[i]); + + return string.Join(".", segments); + } + + /// + /// Returns all IValidationRules that are PropertyRule. + /// If rule is CollectionPropertyRule then isCollectionRule set to true. + /// + internal static IEnumerable GetPropertyRules( + this IEnumerable validationRules) + { + foreach (var validationRule in validationRules) + { + if (validationRule.Member is null || string.IsNullOrEmpty(validationRule.PropertyName)) continue; + var isCollectionRule = validationRule.GetType() == typeof(ICollectionRule<,>); + yield return new ValidationRuleContext(validationRule, isCollectionRule); + } + } + + /// + /// Returns a indicating if the is conditional. + /// + internal static bool HasNoCondition(this IValidationRule propertyRule) => !propertyRule.HasCondition && !propertyRule.HasAsyncCondition; + + /// + /// Contains and additional info. + /// + public readonly struct ValidationRuleContext + { + /// + /// PropertyRule. + /// + public readonly IValidationRule ValidationRule; + + /// + /// Flag indication whether the is the CollectionRule. + /// + public readonly bool IsCollectionRule; + + /// + /// Initializes a new instance of the struct. + /// + /// PropertyRule. + /// Is a CollectionPropertyRule. + public ValidationRuleContext(IValidationRule validationRule, bool isCollectionRule) + { + ValidationRule = validationRule; + IsCollectionRule = isCollectionRule; + } + } +} \ No newline at end of file diff --git a/Biwen.QuickApi/Swagger/ValidationProcessor/FluentValidationRule.cs b/Biwen.QuickApi/Swagger/ValidationProcessor/FluentValidationRule.cs new file mode 100644 index 00000000..c3b2921b --- /dev/null +++ b/Biwen.QuickApi/Swagger/ValidationProcessor/FluentValidationRule.cs @@ -0,0 +1,50 @@ +// Original: https://github.com/zymlabs/nswag-fluentvalidation +// MIT License +// Copyright (c) 2019 Zym Labs LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using FluentValidation.Validators; + +namespace Biwen.QuickApi.Swagger.ValidationProcessor; + +public class FluentValidationRule +{ + /// + /// Rule name. + /// + public string Name { get; } + + /// + /// Predicate to match property validator. + /// + public Func Matches { get; set; } = _ => false; + + /// + /// Modify Swagger schema action. + /// + public Action Apply { get; set; } = _ => + { + }; + + public FluentValidationRule(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/Biwen.QuickApi/Swagger/ValidationProcessor/RuleContext.cs b/Biwen.QuickApi/Swagger/ValidationProcessor/RuleContext.cs new file mode 100644 index 00000000..31ee20c2 --- /dev/null +++ b/Biwen.QuickApi/Swagger/ValidationProcessor/RuleContext.cs @@ -0,0 +1,43 @@ +// Original: https://github.com/zymlabs/nswag-fluentvalidation +// MIT License +// Copyright (c) 2019 Zym Labs LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using FluentValidation.Validators; +using NJsonSchema; + +namespace Biwen.QuickApi.Swagger.ValidationProcessor +{ + public class RuleContext + { + public JsonSchema Schema { get; } + + public string PropertyKey { get; } + + public IPropertyValidator PropertyValidator { get; } + + public RuleContext(JsonSchema schema, string propertyKey, IPropertyValidator propertyValidator) + { + Schema = schema; + PropertyKey = propertyKey; + PropertyValidator = propertyValidator; + } + } +} \ No newline at end of file diff --git a/Biwen.QuickApi/Swagger/ValidationProcessor/readme.md b/Biwen.QuickApi/Swagger/ValidationProcessor/readme.md new file mode 100644 index 00000000..c3d0fed2 --- /dev/null +++ b/Biwen.QuickApi/Swagger/ValidationProcessor/readme.md @@ -0,0 +1 @@ +MIT [参考代码](https://github.com/zymlabs/nswag-fluentvalidation) \ No newline at end of file