diff --git a/Documentation/README.md b/Documentation/README.md index 6b77f525787bb..e8d368b158501 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -19,6 +19,7 @@ - Modelers 5. [Building AutoRest](building-code.md) 6. [Writing Tests](writing-tests.md) +6. [Writing Swagger Validation Rules](writing-validation-rules.md) 7. Contributing to the code [Swagger2.0]:https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md diff --git a/Documentation/writing-validation-rules.md b/Documentation/writing-validation-rules.md new file mode 100644 index 0000000000000..0ab2b42815c91 --- /dev/null +++ b/Documentation/writing-validation-rules.md @@ -0,0 +1,49 @@ +# Writing Swagger Validation Rules + +## Architecture +In the AutoRest pipeline, Swagger files get deserialized into intermediate classes, which are then used to create the language-independent client model. Our validation logic is performed on these deserialization classes to allow logic written in C# to be used to check the object representation of the Swagger spec. + +The root Swagger spec is deserialized into a [`ServiceDefinition`](../src/modeler/AutoRest.Swagger/Model/ServiceDefinition.cs) object. The validation step recursively traverses this object tree and applies the validation rules that apply to each property and consolidate the messages from all rules. Validation rules are associated with a property by decorating it with a `RuleAttribute`. This `RuleAttribute` will be passed the value for that property and determines if that value satisfies the rule or not. Multiple `RuleAttribute` attributes can be applied to the same property, and any rules that fail will be part of the output. + +## Steps for writing a rule (see [instructions below](#instructions)) +1. Define a canonical name that represents the rule and a message that should be shown to the user explaining the validation failure +2. Determine if your rule is an `Info`, a `Warning` or an `Error` +3. Implement the logic that validates this rule against a given object +4. Define where this validation rule gets applied in the object tree +5. Write a test that verifies that this rule correctly validates Swagger specs + +## Instructions +### 1. Add the rule name and message +- The name of your validation rule should be added to the end of the `ValidationExceptionName` enum +- Messages are added to the [`AutoRest.Core.Properties.Resource` resx](../src/core/AutoRest.Core/Properties/Resources.resx). + +### 2. Specify the severity of your validation rule +- Add a mapping that associates your message with the rule name in [`ValidationExceptionConstants`](../src/core/AutoRest.Core/Validation/ValidationExceptionConstants.cs) in either the `Info`, `Warning` or `Error` sections. + +### 3. Add a `Rule` subclass that implements your validation rule logic +- Create a subclass of the `Rule` class, and override the `bool IsValid(object entity)` method. +- For more complex rules (including getting type information in `IsValid()`, see the [Complex rules](#complex-rules) section below. + +### 4. Decorate the appropriate Swagger model property that your rule applies to +- Add a `[Rule(typeof({MyRule})]` attribute above the property that should satisfy the rule. Replace `{MyRule}` with the subclass that you implemented. +- The `typeof()` is necessary because C# doesn't support generics in attributes. + +### 5. Add a test to `SwaggerModelerValidationTests` that validates your validation rule +- Add an incorrect Swagger file to the [`Swagger/Validation/`](../src/modeler/AutoRest.Swagger.Tests/Swagger/Validation) folder that should trigger your validation rule. +- Add a test case to [`SwaggerModelerValidationTests.cs`](../src/modeler/AutoRest.Swagger.Tests/SwaggerModelerValidationTests.cs) that asserts that the validation message returned for the Swagger file is + +## Complex rules +### Typed rules +The `IsValid()` method of the `Rule` class only passes an object with no type information. You can have your rule subclass work on a typed model class by inheriting from the `TypedRule` class instead. By replacing `T` with a model class, your override of `IsValid()` will use `T` as the type for the `entity` parameter. + +### Message interpolation (e.g. `"'foo' is not a valid MIME type for consumes"`) +Simple rules can simply override the `bool IsValid(object entity)` method when subclassing `Rule` and return true or false, depending on if the object satisfies the rule. However, some messages are more useful if they provide the incorrect value as part of the message. + +Rules can override a different `IsValid` overload (`IsValid(object enitity, out object[] formatParameters)`. Any objects returned in `formatParameters` will be passed on to `string.Format()` along with the message associated with the rule. When writing the message, use standard `string.Format()` conventions to define where replacements go (e.g. `"'{0}' is not a valid MIME type for consumes"`). + +### Collection rules +Sometimes, a rule should apply to every item in a list or dictionary, but it cannot be applied to the class definition (since the same class can be used in multiple places in the `ServiceDefinition` tree). + +An example of this is the `AnonymousTypesDiscouraged` rule. The purpose of this rule is to have schemas defined in the `definitions` section of the Swagger file instead of in the parameter that it will be used for. It validates the `Schema` class, but it cannot be applied to all instances of this class, because the `definitions` section also uses the `Schema` class. + +Since we want to apply this rule to parameters in an operation, we can decorate the `Parameters` property of the [`OperationResponse`](../src/modeler/AutoRest.Swagger/Model/Operation.cs) class with the `CollectionRule` attribute. When the object tree is traversed to apply validation rules, each item in the collection will be validated against the `AnonymousParameterTypes` logic. \ No newline at end of file diff --git a/src/core/AutoRest.Core/AutoRest.cs b/src/core/AutoRest.Core/AutoRest.cs index 093fa0d22e183..a4e4b9bf8a3e1 100644 --- a/src/core/AutoRest.Core/AutoRest.cs +++ b/src/core/AutoRest.Core/AutoRest.cs @@ -7,6 +7,9 @@ using AutoRest.Core.Extensibility; using AutoRest.Core.Logging; using AutoRest.Core.Properties; +using AutoRest.Core.Validation; +using System.Collections.Generic; +using System.Linq; namespace AutoRest.Core { @@ -22,7 +25,7 @@ public static string Version { get { - FileVersionInfo fvi = FileVersionInfo.GetVersionInfo((typeof (Settings)).Assembly.Location); + FileVersionInfo fvi = FileVersionInfo.GetVersionInfo((typeof(Settings)).Assembly.Location); return fvi.FileVersion; } } @@ -40,10 +43,22 @@ public static void Generate(Settings settings) Logger.Entries.Clear(); Logger.LogInfo(Resources.AutoRestCore, Version); Modeler modeler = ExtensionsLoader.GetModeler(settings); - ServiceClient serviceClient; + ServiceClient serviceClient = null; + try { - serviceClient = modeler.Build(); + IEnumerable messages = new List(); + serviceClient = modeler.Build(out messages); + + foreach (var message in messages) + { + Logger.Entries.Add(new LogEntry(message.Severity, message.ToString())); + } + + if (messages.Any(entry => entry.Severity >= settings.ValidationLevel)) + { + throw ErrorManager.CreateError(Resources.CodeGenerationError); + } } catch (Exception exception) { diff --git a/src/core/AutoRest.Core/Extensibility/ExtensionsLoader.cs b/src/core/AutoRest.Core/Extensibility/ExtensionsLoader.cs index 6ce9bfc99c894..b3f2d9e3a49e6 100644 --- a/src/core/AutoRest.Core/Extensibility/ExtensionsLoader.cs +++ b/src/core/AutoRest.Core/Extensibility/ExtensionsLoader.cs @@ -38,32 +38,39 @@ public static CodeGenerator GetCodeGenerator(Settings settings) if (string.IsNullOrEmpty(settings.CodeGenerator)) { throw new ArgumentException( - string.Format(CultureInfo.InvariantCulture, + string.Format(CultureInfo.InvariantCulture, Resources.ParameterValueIsMissing, "CodeGenerator")); } CodeGenerator codeGenerator = null; - string configurationFile = GetConfigurationFileContent(settings); - - if (configurationFile != null) + if (string.Equals("None", settings.CodeGenerator, StringComparison.OrdinalIgnoreCase)) { - try + codeGenerator = new NoOpCodeGenerator(settings); + } + else + { + string configurationFile = GetConfigurationFileContent(settings); + + if (configurationFile != null) { - var config = JsonConvert.DeserializeObject(configurationFile); - codeGenerator = LoadTypeFromAssembly(config.CodeGenerators, settings.CodeGenerator, - settings); - codeGenerator.PopulateSettings(settings.CustomSettings); + try + { + var config = JsonConvert.DeserializeObject(configurationFile); + codeGenerator = LoadTypeFromAssembly(config.CodeGenerators, settings.CodeGenerator, + settings); + codeGenerator.PopulateSettings(settings.CustomSettings); + } + catch (Exception ex) + { + throw ErrorManager.CreateError(ex, Resources.ErrorParsingConfig); + } } - catch (Exception ex) + else { - throw ErrorManager.CreateError(ex, Resources.ErrorParsingConfig); + throw ErrorManager.CreateError(Resources.ConfigurationFileNotFound); } } - else - { - throw ErrorManager.CreateError(Resources.ConfigurationFileNotFound); - } Logger.LogInfo(Resources.GeneratorInitialized, settings.CodeGenerator, codeGenerator.GetType().Assembly.GetName().Version); @@ -137,7 +144,7 @@ public static string GetConfigurationFileContent(Settings settings) if (!settings.FileSystem.FileExists(path)) { - path = Path.Combine(Path.GetDirectoryName(Assembly.GetAssembly(typeof (Settings)).Location), + path = Path.Combine(Path.GetDirectoryName(Assembly.GetAssembly(typeof(Settings)).Location), ConfigurationFileName); } @@ -177,10 +184,10 @@ public static T LoadTypeFromAssembly(IDictionary(IDictionary _dict = new Dictionary + { + { LogEntrySeverity.Fatal, ConsoleColor.Red }, + { LogEntrySeverity.Error, ConsoleColor.Red }, + { LogEntrySeverity.Warning, ConsoleColor.Yellow }, + { LogEntrySeverity.Info, ConsoleColor.White }, + }; + + /// + /// Get the console color associated with the severity of the message + /// + /// Severity of the log message. + /// The color to set the console for messages of this severity + public static ConsoleColor GetColorForSeverity(this LogEntrySeverity severity) + { + ConsoleColor color; + if (!_dict.TryGetValue(severity, out color)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "No color defined for severity {0}", severity)); + } + return color; + } + } +} \ No newline at end of file diff --git a/src/core/AutoRest.Core/Logging/Logger.cs b/src/core/AutoRest.Core/Logging/Logger.cs index a577e46fa6462..c0e1e53a76e6e 100644 --- a/src/core/AutoRest.Core/Logging/Logger.cs +++ b/src/core/AutoRest.Core/Logging/Logger.cs @@ -90,68 +90,28 @@ public static void LogError(string message, params object[] args) LogError(null, message, args); } - /// - /// Writes the LogEntry collection to the provided TextWriter. - /// - /// TextWriter for output. - /// If set to true, output includes full exception stack. - public static void WriteErrors(TextWriter writer, bool verbose) + public static void WriteMessages(TextWriter writer, LogEntrySeverity severity) { - if (writer == null) - { - throw new ArgumentNullException("writer"); - } - foreach (var logEntry in Entries.Where(e => e.Severity == LogEntrySeverity.Error || - e.Severity == LogEntrySeverity.Fatal) - .OrderByDescending(e => e.Severity)) - { - string prefix = ""; - if (logEntry.Severity == LogEntrySeverity.Fatal) - { - prefix = "[FATAL] "; - } - writer.WriteLine("error: {0}{1}", prefix, logEntry.Message); - if (logEntry.Exception != null && verbose) - { - writer.WriteLine("{0}", logEntry.Exception); - } - } + WriteMessages(writer, severity, false); } - /// - /// Writes the LogEntrySeverity.Warning messages to the provided TextWriter. - /// - /// TextWriter for output. - public static void WriteWarnings(TextWriter writer) + public static void WriteMessages(TextWriter writer, LogEntrySeverity severity, bool verbose) { if (writer == null) { throw new ArgumentNullException("writer"); } - foreach (var logEntry in Entries.Where(e => e.Severity == LogEntrySeverity.Warning)) + foreach (var logEntry in Entries.Where(e => e.Severity == severity)) { - writer.WriteLine("{0}: {1}", - logEntry.Severity.ToString().ToUpperInvariant(), - logEntry.Message); - } - } - - /// - /// Writes the LogEntrySeverity.Info messages to the provdied TextWriter. - /// - /// TextWriter for output. - public static void WriteInfos(TextWriter writer) - { - if (writer == null) - { - throw new ArgumentNullException("writer"); - } - - foreach (var logEntry in Entries.Where(e => e.Severity == LogEntrySeverity.Info)) - { - writer.WriteLine("{0}: {1}", - logEntry.Severity.ToString().ToUpperInvariant(), + // Write the severity and message to console + writer.WriteLine("{0}: {1}", + logEntry.Severity.ToString().ToUpperInvariant(), logEntry.Message); + // If verbose is on and the entry has an exception, show it + if (logEntry.Exception != null && verbose) + { + writer.WriteLine("{0}", logEntry.Exception); + } } } } diff --git a/src/core/AutoRest.Core/Modeler.cs b/src/core/AutoRest.Core/Modeler.cs index 076d567d3d639..ed096829b21b7 100644 --- a/src/core/AutoRest.Core/Modeler.cs +++ b/src/core/AutoRest.Core/Modeler.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using AutoRest.Core.ClientModel; +using AutoRest.Core.Validation; +using System.Collections.Generic; namespace AutoRest.Core { @@ -17,5 +19,7 @@ protected Modeler(Settings settings) } public abstract ServiceClient Build(); + + public abstract ServiceClient Build(out IEnumerable messages); } } diff --git a/src/core/AutoRest.Core/NoOpCodeGenerator.cs b/src/core/AutoRest.Core/NoOpCodeGenerator.cs new file mode 100644 index 0000000000000..7882e88d7c6cf --- /dev/null +++ b/src/core/AutoRest.Core/NoOpCodeGenerator.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.ClientModel; +using System.Threading.Tasks; + +namespace AutoRest.Core +{ + public class NoOpCodeGenerator: CodeGenerator + { + public NoOpCodeGenerator(Settings settings) : base(settings) + { + } + + public override string Description + { + get + { + return "No op code generator"; + } + } + + public override string ImplementationFileExtension + { + get + { + return string.Empty; + } + } + + public override string Name + { + get + { + return "No op code generator"; + } + } + + public override string UsageInstructions + { + get + { + return string.Empty; + } + } + + public override Task Generate(ServiceClient serviceClient) + { + return Task.FromResult(0); + } + + public override void NormalizeClientModel(ServiceClient serviceClient) + { + } + } +} \ No newline at end of file diff --git a/src/core/AutoRest.Core/Properties/Resources.Designer.cs b/src/core/AutoRest.Core/Properties/Resources.Designer.cs index 76b35b23d65d0..51ab1ab783b27 100644 --- a/src/core/AutoRest.Core/Properties/Resources.Designer.cs +++ b/src/core/AutoRest.Core/Properties/Resources.Designer.cs @@ -10,36 +10,35 @@ namespace AutoRest.Core.Properties { using System; + using System.Reflection; /// - /// A strongly-typed resource class, for looking up localized strings, etc. + /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { + public class Resources { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } /// - /// Returns the cached ResourceManager instance used by this class. + /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoRest.Core.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoRest.Core.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); resourceMan = temp; } return resourceMan; @@ -47,11 +46,11 @@ internal Resources() { } /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -61,219 +60,408 @@ internal Resources() { } /// - /// Looks up a localized string similar to AutoRest Core {0}. + /// Looks up a localized string similar to For better generated code quality, define schemas instead of using anonymous types.. /// - internal static string AutoRestCore { + public static string AnonymousTypesDiscouraged { + get { + return ResourceManager.GetString("AnonymousTypesDiscouraged", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AutoRest Core {0}. + /// + public static string AutoRestCore { get { return ResourceManager.GetString("AutoRestCore", resourceCulture); } } /// - /// Looks up a localized string similar to Code generation failed with errors. See inner exceptions for details.. + /// Looks up a localized string similar to Each body parameter must have a schema. + /// + public static string BodyMustHaveSchema { + get { + return ResourceManager.GetString("BodyMustHaveSchema", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A body parameter cannot have a type, format, or any other properties describing its type.. + /// + public static string BodyWithType { + get { + return ResourceManager.GetString("BodyWithType", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Errors found during Swagger document validation.. + /// + public static string CodeGenerationError { + get { + return ResourceManager.GetString("CodeGenerationError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code generation failed with errors. See inner exceptions for details.. /// - internal static string CodeGenerationFailed { + public static string CodeGenerationFailed { get { return ResourceManager.GetString("CodeGenerationFailed", resourceCulture); } } /// - /// Looks up a localized string similar to Could not load CodeGenSettings file '{0}'. Exception: '{1}'.. + /// Looks up a localized string similar to Could not load CodeGenSettings file '{0}'. Exception: '{1}'.. /// - internal static string CodeGenSettingsFileInvalid { + public static string CodeGenSettingsFileInvalid { get { return ResourceManager.GetString("CodeGenSettingsFileInvalid", resourceCulture); } } /// - /// Looks up a localized string similar to \\\\. + /// Looks up a localized string similar to \\\\. /// - internal static string CommentString { + public static string CommentString { get { return ResourceManager.GetString("CommentString", resourceCulture); } } /// - /// Looks up a localized string similar to AutoRest.json was not found in the current directory. + /// Looks up a localized string similar to AutoRest.json was not found in the current directory. /// - internal static string ConfigurationFileNotFound { + public static string ConfigurationFileNotFound { get { return ResourceManager.GetString("ConfigurationFileNotFound", resourceCulture); } } /// - /// Looks up a localized string similar to Directory {0} does not exist.. + /// Looks up a localized string similar to Properties defined alongside $ref will be ignored according to JSON specification.. /// - internal static string DirectoryNotExist { + public static string ConflictingRef { + get { + return ResourceManager.GetString("ConflictingRef", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value provided for description is not descriptive enough.. + /// + public static string DescriptionNotDescriptive { + get { + return ResourceManager.GetString("DescriptionNotDescriptive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Directory {0} does not exist.. + /// + public static string DirectoryNotExist { get { return ResourceManager.GetString("DirectoryNotExist", resourceCulture); } } /// - /// Looks up a localized string similar to {0} with name '{1}' was renamed to '{2}' because it conflicts with following entities: {3}. + /// Looks up a localized string similar to Empty x-ms-client-name property.. + /// + public static string EmptyClientName { + get { + return ResourceManager.GetString("EmptyClientName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} with name '{1}' was renamed to '{2}' because it conflicts with following entities: {3}. /// - internal static string EntityConflictTitleMessage { + public static string EntityConflictTitleMessage { get { return ResourceManager.GetString("EntityConflictTitleMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Error generating client model: {0}. + /// Looks up a localized string similar to Error generating client model: {0}. /// - internal static string ErrorGeneratingClientModel { + public static string ErrorGeneratingClientModel { get { return ResourceManager.GetString("ErrorGeneratingClientModel", resourceCulture); } } /// - /// Looks up a localized string similar to Error loading {0} assembly: {1}. + /// Looks up a localized string similar to Error loading {0} assembly: {1}. /// - internal static string ErrorLoadingAssembly { + public static string ErrorLoadingAssembly { get { return ResourceManager.GetString("ErrorLoadingAssembly", resourceCulture); } } /// - /// Looks up a localized string similar to Error parsing AutoRest.json file. + /// Looks up a localized string similar to Error parsing AutoRest.json file. /// - internal static string ErrorParsingConfig { + public static string ErrorParsingConfig { get { return ResourceManager.GetString("ErrorParsingConfig", resourceCulture); } } /// - /// Looks up a localized string similar to Error saving generated code: {0}. + /// Looks up a localized string similar to Error saving generated code: {0}. /// - internal static string ErrorSavingGeneratedCode { + public static string ErrorSavingGeneratedCode { get { return ResourceManager.GetString("ErrorSavingGeneratedCode", resourceCulture); } } /// - /// Looks up a localized string similar to Plugin {0} does not have an assembly name in AutoRest.json. + /// Looks up a localized string similar to Plugin {0} does not have an assembly name in AutoRest.json. /// - internal static string ExtensionNotFound { + public static string ExtensionNotFound { get { return ResourceManager.GetString("ExtensionNotFound", resourceCulture); } } /// - /// Looks up a localized string similar to Successfully initialized {0} Code Generator {1}. + /// Looks up a localized string similar to Successfully initialized {0} Code Generator {1}. /// - internal static string GeneratorInitialized { + public static string GeneratorInitialized { get { return ResourceManager.GetString("GeneratorInitialized", resourceCulture); } } /// - /// Looks up a localized string similar to Initializing code generator.. + /// Looks up a localized string similar to Each header parameter should have an explicit client name defined for improved code generation output quality.. + /// + public static string HeaderShouldHaveClientName { + get { + return ResourceManager.GetString("HeaderShouldHaveClientName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Initializing code generator.. /// - internal static string InitializingCodeGenerator { + public static string InitializingCodeGenerator { get { return ResourceManager.GetString("InitializingCodeGenerator", resourceCulture); } } /// - /// Looks up a localized string similar to Initializing modeler.. + /// Looks up a localized string similar to Initializing modeler.. /// - internal static string InitializingModeler { + public static string InitializingModeler { get { return ResourceManager.GetString("InitializingModeler", resourceCulture); } } /// - /// Looks up a localized string similar to Property name {0} cannot be used as an Identifier, as it contains only invalid characters.. + /// Looks up a localized string similar to The default value is not one of the values enumerated as valid for this element.. /// - internal static string InvalidIdentifierName { + public static string InvalidDefault { + get { + return ResourceManager.GetString("InvalidDefault", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property name {0} cannot be used as an Identifier, as it contains only invalid characters.. + /// + public static string InvalidIdentifierName { get { return ResourceManager.GetString("InvalidIdentifierName", resourceCulture); } } /// - /// Looks up a localized string similar to '{0}' code generator does not support code generation to a single file.. + /// Looks up a localized string similar to Only body parameters can have a schema defined.. + /// + public static string InvalidSchemaParameter { + get { + return ResourceManager.GetString("InvalidSchemaParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type {0} and format {1} is not a supported combination. + /// + public static string InvalidTypeFormatCombination { + get { + return ResourceManager.GetString("InvalidTypeFormatCombination", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' code generator does not support code generation to a single file.. /// - internal static string LanguageDoesNotSupportSingleFileGeneration { + public static string LanguageDoesNotSupportSingleFileGeneration { get { return ResourceManager.GetString("LanguageDoesNotSupportSingleFileGeneration", resourceCulture); } } /// - /// Looks up a localized string similar to Successfully initialized modeler {0} v {1}.. + /// Looks up a localized string similar to Consider adding a 'description' element, essential for maintaining reference documentation.. + /// + public static string MissingDescription { + get { + return ResourceManager.GetString("MissingDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to '{0}' is supposedly required, but no such property exists.. + /// + public static string MissingRequiredProperty { + get { + return ResourceManager.GetString("MissingRequiredProperty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Successfully initialized modeler {0} v {1}.. /// - internal static string ModelerInitialized { + public static string ModelerInitialized { get { return ResourceManager.GetString("ModelerInitialized", resourceCulture); } } /// - /// Looks up a localized string similar to {0} (already used in {1}). + /// Looks up a localized string similar to {0} (already used in {1}). /// - internal static string NamespaceConflictReasonMessage { + public static string NamespaceConflictReasonMessage { get { return ResourceManager.GetString("NamespaceConflictReasonMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Please consider changing your swagger specification to avoid naming conflicts.. + /// Looks up a localized string similar to Please consider changing your swagger specification to avoid naming conflicts.. /// - internal static string NamingConflictsSuggestion { + public static string NamingConflictsSuggestion { get { return ResourceManager.GetString("NamingConflictsSuggestion", resourceCulture); } } /// - /// Looks up a localized string similar to Parameter '{0}' is not expected.. + /// Looks up a localized string similar to There is no default response defined in the responses section. + /// + public static string NoDefaultResponse { + get { + return ResourceManager.GetString("NoDefaultResponse", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find a definition for the path parameter '{0}'. + /// + public static string NoDefinitionForPathParameter { + get { + return ResourceManager.GetString("NoDefinitionForPathParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No response objects defined.. /// - internal static string ParameterIsNotValid { + public static string NoResponses { + get { + return ResourceManager.GetString("NoResponses", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only 1 underscore is permitted in the operation id, following Noun_Verb conventions.. + /// + public static string OnlyOneUnderscoreAllowedInOperationId { + get { + return ResourceManager.GetString("OnlyOneUnderscoreAllowedInOperationId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OperationId is required for all operations. Please add it for '{0}' operation of '{1}' path.. + /// + public static string OperationIdMissing { + get { + return ResourceManager.GetString("OperationIdMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Per the Noun_Verb convention for Operation Ids, the noun '{0}' should not appear after the underscore.. + /// + public static string OperationIdNounInVerb { + get { + return ResourceManager.GetString("OperationIdNounInVerb", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter '{0}' is not expected.. + /// + public static string ParameterIsNotValid { get { return ResourceManager.GetString("ParameterIsNotValid", resourceCulture); } } /// - /// Looks up a localized string similar to Parameter '{0}' is required.. + /// Looks up a localized string similar to Parameter '{0}' is required.. /// - internal static string ParameterValueIsMissing { + public static string ParameterValueIsMissing { get { return ResourceManager.GetString("ParameterValueIsMissing", resourceCulture); } } /// - /// Looks up a localized string similar to Parameter '{0}' value is not valid. Expect '{1}'. + /// Looks up a localized string similar to Parameter '{0}' value is not valid. Expect '{1}'. /// - internal static string ParameterValueIsNotValid { + public static string ParameterValueIsNotValid { get { return ResourceManager.GetString("ParameterValueIsNotValid", resourceCulture); } } /// - /// Looks up a localized string similar to Type '{0}' name should be assembly qualified. For example 'ClassName, AssemblyName'. + /// Looks up a localized string similar to Operations can not have more than one 'body' parameter. The following were found: '{0}'. /// - internal static string TypeShouldBeAssemblyQualified { + public static string TooManyBodyParameters { + get { + return ResourceManager.GetString("TooManyBodyParameters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' name should be assembly qualified. For example 'ClassName, AssemblyName'. + /// + public static string TypeShouldBeAssemblyQualified { get { return ResourceManager.GetString("TypeShouldBeAssemblyQualified", resourceCulture); } } + + /// + /// Looks up a localized string similar to The path '{0}' in x-ms-paths does not overload a normal path in the paths section.. + /// + public static string XMSPathBaseNotInPaths { + get { + return ResourceManager.GetString("XMSPathBaseNotInPaths", resourceCulture); + } + } } } diff --git a/src/core/AutoRest.Core/Properties/Resources.resx b/src/core/AutoRest.Core/Properties/Resources.resx index 17f3fe59fa18b..165cc945d86fb 100644 --- a/src/core/AutoRest.Core/Properties/Resources.resx +++ b/src/core/AutoRest.Core/Properties/Resources.resx @@ -189,4 +189,67 @@ Type '{0}' name should be assembly qualified. For example 'ClassName, AssemblyName' + + For better generated code quality, define schemas instead of using anonymous types. + + + Each body parameter must have a schema + + + A body parameter cannot have a type, format, or any other properties describing its type. + + + Properties defined alongside $ref will be ignored according to JSON specification. + + + Empty x-ms-client-name property. + + + Each header parameter should have an explicit client name defined for improved code generation output quality. + + + The default value is not one of the values enumerated as valid for this element. + + + Only body parameters can have a schema defined. + + + Type {0} and format {1} is not a supported combination + + + Consider adding a 'description' element, essential for maintaining reference documentation. + + + '{0}' is supposedly required, but no such property exists. + + + Could not find a definition for the path parameter '{0}' + + + No response objects defined. + + + Only 1 underscore is permitted in the operation id, following Noun_Verb conventions. + + + OperationId is required for all operations. Please add it for '{0}' operation of '{1}' path. + + + Operations can not have more than one 'body' parameter. The following were found: '{0}' + + + Errors found during Swagger document validation. + + + There is no default response defined in the responses section + + + The path '{0}' in x-ms-paths does not overload a normal path in the paths section. + + + The value provided for description is not descriptive enough. + + + Per the Noun_Verb convention for Operation Ids, the noun '{0}' should not appear after the underscore. + \ No newline at end of file diff --git a/src/core/AutoRest.Core/Settings.cs b/src/core/AutoRest.Core/Settings.cs index 1fa0dcdb6441d..2db2cbcc123e4 100644 --- a/src/core/AutoRest.Core/Settings.cs +++ b/src/core/AutoRest.Core/Settings.cs @@ -52,6 +52,7 @@ public Settings() Header = string.Format(CultureInfo.InvariantCulture, DefaultCodeGenerationHeader, AutoRest.Version); CodeGenerator = "CSharp"; Modeler = "Swagger"; + ValidationLevel = LogEntrySeverity.Error; } /// @@ -220,6 +221,15 @@ public string Header [SettingsAlias("cgs")] [SettingsInfo("The path for a json file containing code generation settings.")] public string CodeGenSettings { get; set; } + + /// + /// The input validation severity level that will prevent code generation + /// + [SettingsAlias("vl")] + [SettingsAlias("validation")] + [SettingsInfo("The input validation severity level that will prevent code generation")] + public LogEntrySeverity ValidationLevel { get; set; } + /// /// Factory method to generate CodeGenerationSettings from command line arguments. /// Matches dictionary keys to the settings properties. diff --git a/src/core/AutoRest.Core/Validation/CollectionRuleAttribute.cs b/src/core/AutoRest.Core/Validation/CollectionRuleAttribute.cs new file mode 100644 index 0000000000000..cd89aebf6837f --- /dev/null +++ b/src/core/AutoRest.Core/Validation/CollectionRuleAttribute.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; + +namespace AutoRest.Core.Validation +{ + /// + /// A rule attribute that should be applied to all members of the collection that is annotated + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes")] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true, Inherited = true)] + public class CollectionRuleAttribute : RuleAttribute + { + public CollectionRuleAttribute(Type ruleType) : base(ruleType) + { + } + } +} diff --git a/src/core/AutoRest.Core/Validation/RecursiveObjectValidator.cs b/src/core/AutoRest.Core/Validation/RecursiveObjectValidator.cs new file mode 100644 index 0000000000000..456274bd3ae59 --- /dev/null +++ b/src/core/AutoRest.Core/Validation/RecursiveObjectValidator.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Newtonsoft.Json; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace AutoRest.Core.Validation +{ + /// + /// A validator that traverses an object graph, applies validation rules, and logs validation messages + /// + public class RecursiveObjectValidator + { + public RecursiveObjectValidator() + { + } + + public IEnumerable GetValidationExceptions(object entity) + { + return RecursiveValidate(entity, new List()); + } + + private IEnumerable RecursiveValidate(object entity, IEnumerable collectionRules) + { + if (entity != null) + { + if (entity is IList) + { + // Recursively validate each list item and add the item index to the location of each validation message + IList list = ((IList)entity).Cast().ToList(); + if (list != null) + { + for (int i = 0; i < list.Count; i++) + { + foreach (ValidationMessage exception in RecursiveValidate(list[i], collectionRules)) + { + exception.Path.Add($"[{i}]"); + yield return exception; + } + } + } + } + else if (entity is IDictionary) + { + // Recursively validate each dictionary entry and add the entry key to the location of each validation message + IDictionary dict = ((IDictionary)entity).Cast().ToDictionary(entry => (string)entry.Key, entry => entry.Value); + if (dict != null && entity.IsValidatableDictionary()) + { + foreach (var pair in dict) + { + foreach (ValidationMessage exception in RecursiveValidate(pair.Value, collectionRules)) + { + exception.Path.Add(pair.Key); + yield return exception; + } + } + } + } + else if (entity.GetType().IsClass && entity.GetType() != typeof(string)) + { + // Validate objects by running class rules against the object and recursively against properties + foreach(var exception in ValidateObjectValue(entity, collectionRules)) + { + yield return exception; + } + foreach(var exception in ValidateObjectProperties(entity)) + { + yield return exception; + } + } + } + yield break; + } + + private IEnumerable ValidateObjectValue(object entity, IEnumerable collectionRules) + { + // Get any rules defined for the class of the entity + var classRules = entity.GetType().GetCustomAttributes(true); + + // Combine the class rules with any rules that apply to the collection that the entity is part of + classRules = collectionRules.Concat(classRules); + + // Apply each rule for the entity + return classRules.SelectMany(rule => rule.GetValidationMessages(entity)); + } + + private IEnumerable ValidateObjectProperties(object entity) + { + // Go through each validatable property + foreach (var prop in entity.GetValidatableProperties()) + { + // Get the value of the property from the entity + var value = prop.GetValue(entity); + + // Get any rules defined on this property and apply them to the property value + var propertyRules = prop.GetCustomAttributes(true); + foreach (var rule in propertyRules) + { + foreach (var exception in rule.GetValidationMessages(value)) + { + exception.Path.Add(prop.Name); + yield return exception; + } + } + + // Recursively validate the value of the property (passing any rules to inherit) + var inheritableRules = prop.GetCustomAttributes(true); + foreach (var exception in RecursiveValidate(value, inheritableRules)) + { + exception.Path.Add(prop.Name); + yield return exception; + } + } + yield break; + } + } + + internal static class RulesExtensions + { + private static readonly Type JsonExtensionDataType = typeof(JsonExtensionDataAttribute); + + /// + /// Gets an enumerable of properties for that can be validated + /// + /// The object to get properties for + /// + internal static IEnumerable GetValidatableProperties(this object entity) + { + if (entity == null) + { + return new List(); + } + return entity.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance) + .Where(prop => !Attribute.IsDefined(prop, JsonExtensionDataType)) + .Where(prop => prop.PropertyType != typeof(object)); + } + + /// + /// Determines if a dictionary can be validated by running rules + /// + /// The object to check + /// + internal static bool IsValidatableDictionary(this object entity) + { + if (entity == null) + { + return false; + } + // Dictionaries of type cannot be validated, because the object could be infinitely deep. + // We only want to validate objects that have strong typing for the value type + var dictType = entity.GetType(); + return dictType.IsGenericType && + dictType.GenericTypeArguments.Count() >= 2 && + dictType.GenericTypeArguments[1] != typeof(object); + } + } + +} diff --git a/src/core/AutoRest.Core/Validation/Rule.cs b/src/core/AutoRest.Core/Validation/Rule.cs new file mode 100644 index 0000000000000..b163641e0fa0e --- /dev/null +++ b/src/core/AutoRest.Core/Validation/Rule.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System; +using System.Linq; +using System.Globalization; +using AutoRest.Core.Logging; + +namespace AutoRest.Core.Validation +{ + /// + /// Defines validation logic for an object + /// + public abstract class Rule + { + protected Rule() + { + } + + /// + /// The name of the exception that describes this rule + /// + public abstract ValidationExceptionName Exception { get; } + + /// + /// Returns the validation messages resulting from validating this object + /// + /// The object to validate + /// + public abstract IEnumerable GetValidationMessages(object entity); + + /// + /// Creates an exception for the given , using the format parameters + /// + /// + /// + /// + protected static ValidationMessage CreateException(ValidationExceptionName exceptionName, params object[] messageValues) + { + ValidationMessage validationMessage; + ValidationExceptionName[] ignore = new ValidationExceptionName[] { }; + if (ignore.Any(id => id == exceptionName)) + { + validationMessage = new ValidationMessage() + { + Severity = LogEntrySeverity.Info, + Message = string.Empty + }; + } + else if (ValidationExceptionConstants.Info.Messages.ContainsKey(exceptionName)) + { + validationMessage = new ValidationMessage() + { + Severity = LogEntrySeverity.Info, + Message = string.Format(CultureInfo.InvariantCulture, ValidationExceptionConstants.Info.Messages[exceptionName], messageValues) + }; + } + else if (ValidationExceptionConstants.Warnings.Messages.ContainsKey(exceptionName)) + { + validationMessage = new ValidationMessage() + { + Severity = LogEntrySeverity.Warning, + Message = string.Format(CultureInfo.InvariantCulture, ValidationExceptionConstants.Warnings.Messages[exceptionName], messageValues) + }; + } + else if (ValidationExceptionConstants.Errors.Messages.ContainsKey(exceptionName)) + { + validationMessage = new ValidationMessage() + { + Severity = LogEntrySeverity.Error, + Message = string.Format(CultureInfo.InvariantCulture, ValidationExceptionConstants.Errors.Messages[exceptionName], messageValues) + }; + } + else + { + throw new NotImplementedException(); + } + + validationMessage.ValidationException = exceptionName; + return validationMessage; + } + } +} diff --git a/src/core/AutoRest.Core/Validation/RuleAttribute.cs b/src/core/AutoRest.Core/Validation/RuleAttribute.cs new file mode 100644 index 0000000000000..da3df4fb1e5a2 --- /dev/null +++ b/src/core/AutoRest.Core/Validation/RuleAttribute.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace AutoRest.Core.Validation +{ + /// + /// An attribute that describes a rule to apply to the property or class that it decorates + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes")] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true, Inherited = true)] + public class RuleAttribute : Attribute + { + public Type RuleType { get; } + + private Rule Rule; + + public RuleAttribute(Type ruleType) + { + if (typeof(Rule).IsAssignableFrom(ruleType)) + { + Rule = (Rule)Activator.CreateInstance(ruleType); + } + } + + /// + /// Returns a collection of validation messages for + /// + /// + /// + public virtual IEnumerable GetValidationMessages(object entity) + { + if (Rule != null) + { + foreach(var message in Rule.GetValidationMessages(entity)) + { + yield return message; + } + } + yield break; + } + } +} diff --git a/src/core/AutoRest.Core/Validation/TypedRule.cs b/src/core/AutoRest.Core/Validation/TypedRule.cs new file mode 100644 index 0000000000000..c254b67794ab9 --- /dev/null +++ b/src/core/AutoRest.Core/Validation/TypedRule.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace AutoRest.Core.Validation +{ + /// + /// A rule that validates objects of type + /// + /// The type of the object to validate + public abstract class TypedRule : Rule where T : class + { + protected TypedRule() + { + } + + public sealed override IEnumerable GetValidationMessages(object entity) + { + var typedEntity = entity as T; + if (typedEntity != null) + { + foreach (var exception in GetValidationMessages(typedEntity)) + { + yield return exception; + } + } + yield break; + } + + /// + /// Overridable method that lets a child rule return multiple validation messages for the + /// + /// + /// + public virtual IEnumerable GetValidationMessages(T entity) + { + object[] formatParams; + if (!IsValid(entity, out formatParams)) + { + yield return CreateException(Exception, formatParams); + } + yield break; + } + + /// + /// Overridable method that lets a child rule return objects to be passed to string.Format + /// + /// + /// + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")] + public virtual bool IsValid(T entity, out object[] formatParameters) + { + formatParameters = new object[0]; + return IsValid(entity); + } + + /// + /// Overridable method that lets a child rule specify if passes validation + /// + /// + /// + public virtual bool IsValid(T entity) + { + return true; + } + } +} diff --git a/src/core/AutoRest.Core/Validation/ValidationExceptionConstants.cs b/src/core/AutoRest.Core/Validation/ValidationExceptionConstants.cs new file mode 100644 index 0000000000000..fc3e7c937e612 --- /dev/null +++ b/src/core/AutoRest.Core/Validation/ValidationExceptionConstants.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Properties; +using System.Collections.Generic; + +namespace AutoRest.Core.Validation +{ + public static class ValidationExceptionConstants + { + public static class Info + { + public static readonly IReadOnlyDictionary Messages = new Dictionary + { + { ValidationExceptionName.AnonymousTypesDiscouraged, Resources.AnonymousTypesDiscouraged }, + }; + } + + public static class Warnings + { + public static readonly IReadOnlyDictionary Messages = new Dictionary + { + { ValidationExceptionName.DescriptionRequired, Resources.MissingDescription }, + { ValidationExceptionName.NonEmptyClientName, Resources.EmptyClientName }, + { ValidationExceptionName.RefsMustNotHaveSiblings, Resources.ConflictingRef }, + { ValidationExceptionName.DefaultResponseRequired, Resources.NoDefaultResponse }, + { ValidationExceptionName.XmsPathsMustOverloadPaths, Resources.XMSPathBaseNotInPaths }, + { ValidationExceptionName.DescriptiveDescription, Resources.DescriptionNotDescriptive }, + { ValidationExceptionName.OperationIdNounsNotInVerbs, Resources.OperationIdNounInVerb }, + }; + } + + public static class Errors + { + public static readonly IReadOnlyDictionary Messages = new Dictionary + { + { ValidationExceptionName.DefaultMustBeInEnum, Resources.InvalidDefault }, + { ValidationExceptionName.OneUnderscoreInOperationId, Resources.OnlyOneUnderscoreAllowedInOperationId }, + }; + } + } +} diff --git a/src/core/AutoRest.Core/Validation/ValidationExceptionName.cs b/src/core/AutoRest.Core/Validation/ValidationExceptionName.cs new file mode 100644 index 0000000000000..54994fc0b10e3 --- /dev/null +++ b/src/core/AutoRest.Core/Validation/ValidationExceptionName.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace AutoRest.Core.Validation +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xms")] + public enum ValidationExceptionName + { + None = 0, + DescriptionRequired, + NonEmptyClientName, + DefaultMustBeInEnum, + RefsMustNotHaveSiblings, + AnonymousTypesDiscouraged, + OneUnderscoreInOperationId, + DefaultResponseRequired, + XmsPathsMustOverloadPaths, + DescriptiveDescription, + OperationIdNounsNotInVerbs, + } +} \ No newline at end of file diff --git a/src/core/AutoRest.Core/Validation/ValidationMessage.cs b/src/core/AutoRest.Core/Validation/ValidationMessage.cs new file mode 100644 index 0000000000000..6b4fb2ad63065 --- /dev/null +++ b/src/core/AutoRest.Core/Validation/ValidationMessage.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Logging; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace AutoRest.Core.Validation +{ + public class ValidationMessage + { + private IList _path = new List(); + + public ValidationExceptionName ValidationException { get; set; } + + public string Message { get; set; } + + public LogEntrySeverity Severity { get; set; } + + public IList Path + { + get { return this._path; } + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}: {1}\n Location: Path: {2}", ValidationException, Message, string.Join("->", Path.Reverse())); + } + } +} diff --git a/src/core/AutoRest/Program.cs b/src/core/AutoRest/Program.cs index c3a214333d5fa..01f7f57f8fe5d 100644 --- a/src/core/AutoRest/Program.cs +++ b/src/core/AutoRest/Program.cs @@ -60,35 +60,44 @@ private static int Main(string[] args) { if (Logger.Entries.Any(e => e.Severity == LogEntrySeverity.Error || e.Severity == LogEntrySeverity.Fatal)) { - Console.WriteLine(Resources.GenerationFailed); - Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0} {1}", - typeof(Program).Assembly.ManifestModule.Name, - string.Join(" ", args))); + if (!string.Equals("None", settings.CodeGenerator, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(Resources.GenerationFailed); + Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0} {1}", + typeof(Program).Assembly.ManifestModule.Name, + string.Join(" ", args))); + } } else { - Console.WriteLine(Resources.GenerationComplete, - settings.CodeGenerator, settings.Input); + if (!string.Equals("None", settings.CodeGenerator, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(Resources.GenerationComplete, + settings.CodeGenerator, settings.Input); + } exitCode = (int)ExitCode.Success; } } + // Write all messages to Console Console.ResetColor(); - // Include LogEntrySeverity.Infos for verbose logging. - if (args.Any(a => "-Verbose".Equals(a, StringComparison.OrdinalIgnoreCase))) + var validationLevel = settings?.ValidationLevel ?? LogEntrySeverity.Error; + var shouldShowVerbose = settings?.Verbose ?? false; + foreach (var severity in (LogEntrySeverity[])Enum.GetValues(typeof(LogEntrySeverity))) { - Console.ForegroundColor = ConsoleColor.White; - Logger.WriteInfos(Console.Out); + // Set the color if the severity level has a set console color + Console.ForegroundColor = severity.GetColorForSeverity(); + // Determine if this severity of messages should be treated as errors + bool isErrorMessage = severity >= validationLevel; + // Set the output stream based on if the severity should be an error or not + var outputStream = isErrorMessage ? Console.Error : Console.Out; + // If it's an error level severity or we want to see all output, write to console + if (isErrorMessage || shouldShowVerbose) + { + Logger.WriteMessages(outputStream, severity, shouldShowVerbose); + } + Console.ResetColor(); } - - Console.ForegroundColor = ConsoleColor.Yellow; - Logger.WriteWarnings(Console.Out); - - Console.ForegroundColor = ConsoleColor.Red; - Logger.WriteErrors(Console.Error, - args.Any(a => "-Verbose".Equals(a, StringComparison.OrdinalIgnoreCase))); - - Console.ResetColor(); } } catch (Exception exception) diff --git a/src/modeler/AutoRest.CompositeSwagger/CompositeSwaggerModeler.cs b/src/modeler/AutoRest.CompositeSwagger/CompositeSwaggerModeler.cs index 09bd65e514dc0..ea08e4c035ba6 100644 --- a/src/modeler/AutoRest.CompositeSwagger/CompositeSwaggerModeler.cs +++ b/src/modeler/AutoRest.CompositeSwagger/CompositeSwaggerModeler.cs @@ -13,6 +13,8 @@ using AutoRest.Swagger; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using AutoRest.Core.Validation; +using System.Collections.Generic; namespace AutoRest.CompositeSwagger { @@ -271,5 +273,12 @@ private static void AssertEquals(T compositeProperty, T subProperty, string p } } } + + public override ServiceClient Build(out IEnumerable messages) + { + // No composite modeler validation messages yet + messages = new List(); + return Build(); + } } } diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/anonymous-parameter-type.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/anonymous-parameter-type.json new file mode 100644 index 0000000000000..542b31565e39e --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/anonymous-parameter-type.json @@ -0,0 +1,30 @@ +{ + "swagger": "2.0", + "info": { + "title": "Consumes has an unsupported MIME type", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": { + "test": { + "name": "PetCreateOrUpdateParameter", + "description": "test", + "in": "body", + "schema": { + "type": "string", + "description": "error" + } + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/anonymous-response-type.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/anonymous-response-type.json new file mode 100644 index 0000000000000..cb50106c449dd --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/anonymous-response-type.json @@ -0,0 +1,37 @@ +{ + "swagger": "2.0", + "info": { + "title": "Consumes has an unsupported MIME type", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "responses": { + "default": { + "description": "Unexpected error", + "schema": { + "type": "string", + "description": "error" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/default-value-not-in-enum.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/default-value-not-in-enum.json new file mode 100644 index 0000000000000..311be69dfa1a8 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/default-value-not-in-enum.json @@ -0,0 +1,50 @@ +{ + "swagger": "2.0", + "info": { + "title": "Default value for an ObjectSchema does not appear in enum constraint", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "responses": { + "default": { + "description": "Unexpected error" + } + } + } + } + }, + "definitions": { + "Test1": { + "description": "Property for foo path 1" + }, + "Test2": { + "description": "Property for foo path 2" + }, + "Test": { + "type": "string", + "description": "Property for foo path", + "enum": [ + "Foo", + "Bar" + ], + "default": "Baz" + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/definition-missing-description.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/definition-missing-description.json new file mode 100644 index 0000000000000..43fa06d0a5930 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/definition-missing-description.json @@ -0,0 +1,39 @@ +{ + "swagger": "2.0", + "info": { + "title": "Definition missing description", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "responses": { + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + } + }, + "definitions": { + "Error": {} + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/empty-client-name-extension.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/empty-client-name-extension.json new file mode 100644 index 0000000000000..66b836160b575 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/empty-client-name-extension.json @@ -0,0 +1,44 @@ +{ + "swagger": "2.0", + "info": { + "title": "Consumes has an unsupported MIME type", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo/{test}": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "parameters": [ + { + "name": "test", + "in": "path", + "required": true, + "type": "string", + "format": "wrong", + "description": "test parameter", + "x-ms-client-name": "" + } + ], + "responses": { + "default": { + "description": "Unexpected error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/invalid-format.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/invalid-format.json new file mode 100644 index 0000000000000..2dc207269a3b5 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/invalid-format.json @@ -0,0 +1,43 @@ +{ + "swagger": "2.0", + "info": { + "title": "Consumes has an unsupported MIME type", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo/{test}": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "parameters": [ + { + "name": "test", + "in": "path", + "required": true, + "type": "string", + "format": "wrong", + "description": "test parameter" + } + ], + "responses": { + "default": { + "description": "Unexpected error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operation-group-underscores.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operation-group-underscores.json new file mode 100644 index 0000000000000..f4d1857271f15 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operation-group-underscores.json @@ -0,0 +1,33 @@ +{ + "swagger": "2.0", + "info": { + "title": "Consumes has an unsupported MIME type", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "Noun_Verb_Extra", + "summary": "Foo path", + "description": "Foo operation", + "responses": { + "default": { + "description": "Unexpected error" + } + } + } + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-multiple-body-parameters.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-multiple-body-parameters.json new file mode 100644 index 0000000000000..d9a7d2005c662 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-multiple-body-parameters.json @@ -0,0 +1,58 @@ +{ + "swagger": "2.0", + "info": { + "title": "Operation with multiple body parameters", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "parameters": [ + { + "name": "test", + "in": "body", + "description": "test parameter", + "required": true, + "schema": { + "$ref": "#/definitions/Error" + } + }, + { + "name": "test2", + "in": "body", + "required": true, + "description": "test parameter 2", + "schema": { + "$ref": "#/definitions/Error" + } + } + ], + "responses": { + "default": { + "description": "Unexpected error" + } + } + } + } + }, + "definitions": { + "Error": { + "description": "Test" + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-no-default-response.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-no-default-response.json new file mode 100644 index 0000000000000..1f7a3797d22e1 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-no-default-response.json @@ -0,0 +1,38 @@ +{ + "swagger": "2.0", + "info": { + "title": "Operation with multiple body parameters", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "Error": { + "description": "Test" + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-no-responses.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-no-responses.json new file mode 100644 index 0000000000000..ac14244978926 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/operations-no-responses.json @@ -0,0 +1,34 @@ +{ + "swagger": "2.0", + "info": { + "title": "Operation with multiple body parameters", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "responses": {} + } + } + }, + "definitions": { + "Error": { + "description": "Test" + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/ref-sibling-properties.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/ref-sibling-properties.json new file mode 100644 index 0000000000000..85825ce8ff4e3 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/ref-sibling-properties.json @@ -0,0 +1,50 @@ +{ + "swagger": "2.0", + "info": { + "title": "Consumes has an unsupported MIME type", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo/{test}": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "parameters": [ + { + "name": "test", + "in": "body", + "required": true, + "description": "Test parameter", + "schema": { + "$ref": "#/definitions/Error", + "description": "Default error override" + } + } + ], + "responses": { + "default": { + "description": "Unexpected error" + } + } + } + } + }, + "definitions": { + "Error": { + "description": "Default error" + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/required-property-not-in-properties.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/required-property-not-in-properties.json new file mode 100644 index 0000000000000..465369b531119 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/required-property-not-in-properties.json @@ -0,0 +1,50 @@ +{ + "swagger": "2.0", + "info": { + "title": "Definition missing description", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "PostFoo", + "summary": "Foo path", + "description": "Foo operation", + "responses": { + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + } + }, + "definitions": { + "Error": { + "description": "Default error", + "required": [ + "foo" + ], + "properties": { + "bar": { + "description": "bar property", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/xms-path-not-in-paths.json b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/xms-path-not-in-paths.json new file mode 100644 index 0000000000000..2f522305be2d5 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/Swagger/Validation/xms-path-not-in-paths.json @@ -0,0 +1,45 @@ +{ + "swagger": "2.0", + "info": { + "title": "Consumes has an unsupported MIME type", + "description": "Some documentation.", + "version": "2014-04-01-preview" + }, + "host": "management.azure.com", + "schemes": [ + "https" + ], + "basePath": "/", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "paths": { + "/foo": { + "post": { + "operationId": "Foo_Post", + "description": "Food post", + "responses": { + "default": { + "description": "Default response" + } + } + } + } + }, + "x-ms-paths": { + "/bar?op=baz": { + "post": { + "operationId": "Bar_Post", + "description": "Baz operation on bar", + "responses": { + "default": { + "description": "Default response" + } + } + } + } + } +} diff --git a/src/modeler/AutoRest.Swagger.Tests/SwaggerModelerValidationTests.cs b/src/modeler/AutoRest.Swagger.Tests/SwaggerModelerValidationTests.cs new file mode 100644 index 0000000000000..a5a46fecdb6b1 --- /dev/null +++ b/src/modeler/AutoRest.Swagger.Tests/SwaggerModelerValidationTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.IO; +using System.Linq; +using Xunit; +using System.Collections.Generic; +using AutoRest.Core.Validation; +using AutoRest.Core.Logging; +using AutoRest.Core; + +namespace AutoRest.Swagger.Tests +{ + internal static class AssertExtensions + { + internal static void AssertOnlyValidationWarning(this IEnumerable messages, ValidationExceptionName exception) + { + AssertOnlyValidationMessage(messages.Where(m => m.Severity == LogEntrySeverity.Warning), exception); + } + + internal static void AssertOnlyValidationMessage(this IEnumerable messages, ValidationExceptionName exception) + { + Assert.Equal(1, messages.Count()); + Assert.Equal(exception, messages.First().ValidationException); + } + } + + [Collection("Validation Tests")] + public class SwaggerModelerValidationTests + { + private IEnumerable ValidateSwagger(string input) + { + var modeler = new SwaggerModeler(new Settings + { + Namespace = "Test", + Input = input + }); + IEnumerable messages = new List(); + modeler.Build(out messages); + return messages; + } + + [Fact] + public void MissingDescriptionValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "definition-missing-description.json")); + messages.AssertOnlyValidationMessage(ValidationExceptionName.DescriptionRequired); + } + + [Fact] + public void DefaultValueInEnumValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "default-value-not-in-enum.json")); + messages.AssertOnlyValidationMessage(ValidationExceptionName.DefaultMustBeInEnum); + } + + [Fact] + public void EmptyClientNameValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "empty-client-name-extension.json")); + messages.AssertOnlyValidationWarning(ValidationExceptionName.NonEmptyClientName); + } + + [Fact] + public void RefSiblingPropertiesValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "ref-sibling-properties.json")); + messages.AssertOnlyValidationWarning(ValidationExceptionName.RefsMustNotHaveSiblings); + } + + [Fact] + public void NoResponsesValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "operations-no-responses.json")); + messages.AssertOnlyValidationMessage(ValidationExceptionName.DefaultResponseRequired); + } + + [Fact] + public void AnonymousSchemasDiscouragedValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "anonymous-response-type.json")); + messages.AssertOnlyValidationMessage(ValidationExceptionName.AnonymousTypesDiscouraged); + } + + [Fact] + public void AnonymousParameterSchemaValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "anonymous-parameter-type.json")); + messages.AssertOnlyValidationMessage(ValidationExceptionName.AnonymousTypesDiscouraged); + } + + [Fact] + public void OperationGroupSingleUnderscoreValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "operation-group-underscores.json")); + messages.AssertOnlyValidationMessage(ValidationExceptionName.OneUnderscoreInOperationId); + } + + [Fact] + public void MissingDefaultResponseValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "operations-no-default-response.json")); + messages.AssertOnlyValidationMessage(ValidationExceptionName.DefaultResponseRequired); + } + + [Fact] + public void XMSPathNotInPathsValidation() + { + var messages = ValidateSwagger(Path.Combine("Swagger", "Validation", "xms-path-not-in-paths.json")); + messages.AssertOnlyValidationMessage(ValidationExceptionName.XmsPathsMustOverloadPaths); + } + } +} diff --git a/src/modeler/AutoRest.Swagger/Model/Operation.cs b/src/modeler/AutoRest.Swagger/Model/Operation.cs index 40efbda9283f4..03322ef3e3213 100644 --- a/src/modeler/AutoRest.Swagger/Model/Operation.cs +++ b/src/modeler/AutoRest.Swagger/Model/Operation.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using AutoRest.Core.Validation; +using AutoRest.Swagger.Validation; using System.Collections.Generic; namespace AutoRest.Swagger.Model @@ -26,6 +28,8 @@ public Operation() /// operations described in the API. Tools and libraries MAY use the /// operation id to uniquely identify an operation. /// + [Rule(typeof(OperationIdSingleUnderscore))] + [Rule(typeof(OperationIdNounInVerb))] public string OperationId { get; set; } public string Summary { get; set; } @@ -52,11 +56,13 @@ public Operation() /// If a parameter is already defined at the Path Item, the /// new definition will override it, but can never remove it. /// + [CollectionRule(typeof(AnonymousParameterTypes))] public IList Parameters { get; set; } /// /// The list of possible responses as they are returned from executing this operation. /// + [Rule(typeof(DefaultResponseRequired))] public Dictionary Responses { get; set; } /// diff --git a/src/modeler/AutoRest.Swagger/Model/Response.cs b/src/modeler/AutoRest.Swagger/Model/Response.cs index 4ccc9651739bf..78eccb8e0983c 100644 --- a/src/modeler/AutoRest.Swagger/Model/Response.cs +++ b/src/modeler/AutoRest.Swagger/Model/Response.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using AutoRest.Core.Validation; +using AutoRest.Swagger.Validation; using System; using System.Collections.Generic; @@ -10,10 +12,11 @@ namespace AutoRest.Swagger.Model /// Describes a single response from an API Operation. /// [Serializable] - public class OperationResponse + public class OperationResponse : SwaggerBase { public string Description { get; set; } + [Rule(typeof(AnonymousTypes))] public Schema Schema { get; set; } public Dictionary Headers { get; set; } diff --git a/src/modeler/AutoRest.Swagger/Model/Schema.cs b/src/modeler/AutoRest.Swagger/Model/Schema.cs index fdd39d5af0fec..abadbfd674079 100644 --- a/src/modeler/AutoRest.Swagger/Model/Schema.cs +++ b/src/modeler/AutoRest.Swagger/Model/Schema.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; namespace AutoRest.Swagger.Model { @@ -30,9 +29,6 @@ public class Schema : SwaggerObject /// public Dictionary Properties { get; set; } - [JsonProperty(PropertyName = "$ref")] - public string Reference { get; set; } - public bool ReadOnly { get; set; } public ExternalDoc ExternalDocs { get; set; } diff --git a/src/modeler/AutoRest.Swagger/Model/ServiceDefinition.cs b/src/modeler/AutoRest.Swagger/Model/ServiceDefinition.cs index 797e913e801fc..ecc2ff4fb74fd 100644 --- a/src/modeler/AutoRest.Swagger/Model/ServiceDefinition.cs +++ b/src/modeler/AutoRest.Swagger/Model/ServiceDefinition.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using Newtonsoft.Json; +using AutoRest.Core.Validation; +using AutoRest.Swagger.Validation; namespace AutoRest.Swagger.Model { @@ -13,6 +15,7 @@ namespace AutoRest.Swagger.Model /// Swagger Object - https://github.com/wordnik/swagger-spec/blob/master/versions/2.0.md#swagger-object- /// [Serializable] + [Rule(typeof(XmsPathsInPath))] public class ServiceDefinition : SpecObject { public ServiceDefinition() @@ -86,6 +89,7 @@ public ServiceDefinition() /// Dictionary of parameters that can be used across operations. /// This property does not define global parameters for all operations. /// + [CollectionRule(typeof(AnonymousParameterTypes))] public Dictionary Parameters { get; set; } /// diff --git a/src/modeler/AutoRest.Swagger/Model/SwaggerBase.cs b/src/modeler/AutoRest.Swagger/Model/SwaggerBase.cs index bb6f63a825f78..15af82bfd60e9 100644 --- a/src/modeler/AutoRest.Swagger/Model/SwaggerBase.cs +++ b/src/modeler/AutoRest.Swagger/Model/SwaggerBase.cs @@ -4,11 +4,14 @@ using System; using System.Collections.Generic; using Newtonsoft.Json; +using AutoRest.Core.Validation; +using AutoRest.Swagger.Validation; namespace AutoRest.Swagger.Model { [Serializable] - public class SwaggerBase + [Rule(typeof(ClientNameRequired))] + public abstract class SwaggerBase { public SwaggerBase() { diff --git a/src/modeler/AutoRest.Swagger/Model/SwaggerObject.cs b/src/modeler/AutoRest.Swagger/Model/SwaggerObject.cs index 6285601caa11c..cae98fa92e720 100644 --- a/src/modeler/AutoRest.Swagger/Model/SwaggerObject.cs +++ b/src/modeler/AutoRest.Swagger/Model/SwaggerObject.cs @@ -6,6 +6,9 @@ using System.Globalization; using AutoRest.Core.ClientModel; using AutoRest.Swagger.Properties; +using AutoRest.Core.Validation; +using AutoRest.Swagger.Validation; +using Newtonsoft.Json; namespace AutoRest.Swagger.Model { @@ -14,6 +17,9 @@ namespace AutoRest.Swagger.Model /// https://github.com/wordnik/swagger-spec/blob/master/versions/2.0.md#parameterObject /// [Serializable] + [Rule(typeof(DescriptionRequired))] + [Rule(typeof(EnumContainsDefault))] + [Rule(typeof(RefNoSiblings))] public abstract class SwaggerObject : SwaggerBase { public virtual bool IsRequired { get; set; } @@ -33,11 +39,15 @@ public abstract class SwaggerObject : SwaggerBase /// public virtual Schema Items { get; set; } + [JsonProperty(PropertyName = "$ref")] + public string Reference { get; set; } + /// /// Describes the type of additional properties in the data type. /// public virtual Schema AdditionalProperties { get; set; } + [Rule(typeof(DescriptiveDescriptionRequired))] public virtual string Description { get; set; } /// @@ -147,7 +157,7 @@ public PrimaryType ToType() return new PrimaryType(KnownPrimaryType.Stream); default: throw new NotImplementedException( - string.Format(CultureInfo.InvariantCulture, + string.Format(CultureInfo.InvariantCulture, Resources.InvalidTypeInSwaggerSchema, Type)); } diff --git a/src/modeler/AutoRest.Swagger/Model/SwaggerParameter.cs b/src/modeler/AutoRest.Swagger/Model/SwaggerParameter.cs index 04094bd53aaeb..a9ddc88b80417 100644 --- a/src/modeler/AutoRest.Swagger/Model/SwaggerParameter.cs +++ b/src/modeler/AutoRest.Swagger/Model/SwaggerParameter.cs @@ -8,7 +8,7 @@ namespace AutoRest.Swagger.Model { /// /// Describes a single operation parameter. - /// https://github.com/wordnik/swagger-spec/blob/master/versions/2.0.md#parameterObject + /// https://github.com/wordnik/swagger-spec/blob/master/versions/2.0.md#parameterObject /// [Serializable] public class SwaggerParameter : SwaggerObject @@ -18,9 +18,6 @@ public class SwaggerParameter : SwaggerObject public ParameterLocation In { get; set; } - [JsonProperty(PropertyName = "$ref")] - public string Reference { get; set; } - [JsonProperty(PropertyName = "required")] public override bool IsRequired { diff --git a/src/modeler/AutoRest.Swagger/Model/Tag.cs b/src/modeler/AutoRest.Swagger/Model/Tag.cs index a472e1b15f7b5..575134572dbbd 100644 --- a/src/modeler/AutoRest.Swagger/Model/Tag.cs +++ b/src/modeler/AutoRest.Swagger/Model/Tag.cs @@ -6,7 +6,7 @@ namespace AutoRest.Swagger.Model /// /// Represents a Swagger Tag /// - public class Tag + public class Tag : SwaggerBase { public string Name { get; set; } public string Description { get; set; } diff --git a/src/modeler/AutoRest.Swagger/Properties/Resources.Designer.cs b/src/modeler/AutoRest.Swagger/Properties/Resources.Designer.cs index 535df29cb9723..1617d8b2c8e4b 100644 --- a/src/modeler/AutoRest.Swagger/Properties/Resources.Designer.cs +++ b/src/modeler/AutoRest.Swagger/Properties/Resources.Designer.cs @@ -10,36 +10,35 @@ namespace AutoRest.Swagger.Properties { using System; + using System.Reflection; /// - /// A strongly-typed resource class, for looking up localized strings, etc. + /// A strongly-typed resource class, for looking up localized strings, etc. /// // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { + public class Resources { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } /// - /// Returns the cached ResourceManager instance used by this class. + /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoRest.Swagger.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoRest.Swagger.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); resourceMan = temp; } return resourceMan; @@ -47,11 +46,11 @@ internal Resources() { } /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -61,216 +60,225 @@ internal Resources() { } /// - /// Looks up a localized string similar to Found a type set '{0}' which is circularly defined.. + /// Looks up a localized string similar to Found a type set '{0}' which is circularly defined.. /// - internal static string CircularBaseSchemaSet { + public static string CircularBaseSchemaSet { get { return ResourceManager.GetString("CircularBaseSchemaSet", resourceCulture); } } /// - /// Looks up a localized string similar to Circular reference detected: {0}. + /// Looks up a localized string similar to Circular reference detected: {0}. /// - internal static string CircularReference { + public static string CircularReference { get { return ResourceManager.GetString("CircularReference", resourceCulture); } } /// - /// Looks up a localized string similar to Reference specifies the definition {0} that does not exist.. + /// Looks up a localized string similar to Reference specifies the definition {0} that does not exist.. /// - internal static string DefinitionDoesNotExist { + public static string DefinitionDoesNotExist { get { return ResourceManager.GetString("DefinitionDoesNotExist", resourceCulture); } } /// - /// Looks up a localized string similar to Found operation objects with duplicate operationId '{0}'. OperationId must be unique among all operations described in the API.. + /// Looks up a localized string similar to Found operation objects with duplicate operationId '{0}'. OperationId must be unique among all operations described in the API.. /// - internal static string DuplicateOperationIdException { + public static string DuplicateOperationIdException { get { return ResourceManager.GetString("DuplicateOperationIdException", resourceCulture); } } /// - /// Looks up a localized string similar to Error parsing swagger file. + /// Looks up a localized string similar to Error parsing swagger file. /// - internal static string ErrorParsingSpec { + public static string ErrorParsingSpec { get { return ResourceManager.GetString("ErrorParsingSpec", resourceCulture); } } /// - /// Looks up a localized string similar to Reached Maximum reference depth when resolving reference '{0}'.. + /// Looks up a localized string similar to Reached Maximum reference depth when resolving reference '{0}'.. /// - internal static string ExceededMaximumReferenceDepth { + public static string ExceededMaximumReferenceDepth { get { return ResourceManager.GetString("ExceededMaximumReferenceDepth", resourceCulture); } } /// - /// Looks up a localized string similar to Generating client model from swagger model.. + /// Looks up a localized string similar to Generating client model from swagger model.. /// - internal static string GeneratingClient { + public static string GeneratingClient { get { return ResourceManager.GetString("GeneratingClient", resourceCulture); } } /// - /// Looks up a localized string similar to Found incompatible property types {1}, {2} for property '{0}' in schema inheritance chain {3}. + /// Looks up a localized string similar to Found incompatible property types {1}, {2} for property '{0}' in schema inheritance chain {3}. /// - internal static string IncompatibleTypesInBaseSchema { + public static string IncompatibleTypesInBaseSchema { get { return ResourceManager.GetString("IncompatibleTypesInBaseSchema", resourceCulture); } } /// - /// Looks up a localized string similar to Found incompatible property types {1}, {2} for property '{0}' in schema {3}. + /// Looks up a localized string similar to Found incompatible property types {1}, {2} for property '{0}' in schema {3}. /// - internal static string IncompatibleTypesInSchemaComposition { + public static string IncompatibleTypesInSchemaComposition { get { return ResourceManager.GetString("IncompatibleTypesInSchemaComposition", resourceCulture); } } /// - /// Looks up a localized string similar to Swagger specification is missing info section. + /// Looks up a localized string similar to Swagger specification is missing info section. /// - internal static string InfoSectionMissing { + public static string InfoSectionMissing { get { return ResourceManager.GetString("InfoSectionMissing", resourceCulture); } } /// - /// Looks up a localized string similar to The schema's '{0}' ancestors should have at lease one property. + /// Looks up a localized string similar to Input parameter is required.. /// - internal static string InvalidAncestors { + public static string InputRequired { + get { + return ResourceManager.GetString("InputRequired", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The schema's '{0}' ancestors should have at lease one property. + /// + public static string InvalidAncestors { get { return ResourceManager.GetString("InvalidAncestors", resourceCulture); } } /// - /// Looks up a localized string similar to Collection format '{0}' is not a valid collection format (in parameter '{1}').. + /// Looks up a localized string similar to Collection format '{0}' is not a valid collection format (in parameter '{1}').. /// - internal static string InvalidCollectionFormat { + public static string InvalidCollectionFormat { get { return ResourceManager.GetString("InvalidCollectionFormat", resourceCulture); } } /// - /// Looks up a localized string similar to Cannot use 'extend' property with 'allOf' property in schema {0}. + /// Looks up a localized string similar to Cannot use 'extend' property with 'allOf' property in schema {0}. /// - internal static string InvalidTypeExtendsWithAllOf { + public static string InvalidTypeExtendsWithAllOf { get { return ResourceManager.GetString("InvalidTypeExtendsWithAllOf", resourceCulture); } } /// - /// Looks up a localized string similar to '{0}' is not implemented in SwaggerSchema.ToType extension method.. + /// Looks up a localized string similar to '{0}' is not implemented in SwaggerSchema.ToType extension method.. /// - internal static string InvalidTypeInSwaggerSchema { + public static string InvalidTypeInSwaggerSchema { get { return ResourceManager.GetString("InvalidTypeInSwaggerSchema", resourceCulture); } } /// - /// Looks up a localized string similar to Collection format "multi" is not supported (in parameter '{0}').. + /// Looks up a localized string similar to Collection format "multi" is not supported (in parameter '{0}').. /// - internal static string MultiCollectionFormatNotSupported { + public static string MultiCollectionFormatNotSupported { get { return ResourceManager.GetString("MultiCollectionFormatNotSupported", resourceCulture); } } /// - /// Looks up a localized string similar to Method '{0}' does not declare any MIME type for the return body. Generated code will not deserialize the content.. + /// Looks up a localized string similar to Method '{0}' does not declare any MIME type for the return body. Generated code will not deserialize the content.. /// - internal static string NoProduceOperationWithBody { + public static string NoProduceOperationWithBody { get { return ResourceManager.GetString("NoProduceOperationWithBody", resourceCulture); } } /// - /// Looks up a localized string similar to OperationId is required for all operations. Please add it for '{0}' operation of '{1}' path. . + /// Looks up a localized string similar to OperationId is required for all operations. Please add it for '{0}' operation of '{1}' path. . /// - internal static string OperationIdMissing { + public static string OperationIdMissing { get { return ResourceManager.GetString("OperationIdMissing", resourceCulture); } } /// - /// Looks up a localized string similar to Options HTTP verb is not supported.. + /// Looks up a localized string similar to Options HTTP verb is not supported.. /// - internal static string OptionsNotSupported { + public static string OptionsNotSupported { get { return ResourceManager.GetString("OptionsNotSupported", resourceCulture); } } /// - /// Looks up a localized string similar to Parsing swagger json file.. + /// Looks up a localized string similar to Parsing swagger json file.. /// - internal static string ParsingSwagger { + public static string ParsingSwagger { get { return ResourceManager.GetString("ParsingSwagger", resourceCulture); } } /// - /// Looks up a localized string similar to Property '{0}' in Model '{1}' is marked readOnly and is also required. This is not allowed.. + /// Looks up a localized string similar to Property '{0}' in Model '{1}' is marked readOnly and is also required. This is not allowed.. /// - internal static string ReadOnlyNotRequired { + public static string ReadOnlyNotRequired { get { return ResourceManager.GetString("ReadOnlyNotRequired", resourceCulture); } } /// - /// Looks up a localized string similar to Reference path '{0}' does not exist in the definition section of the Swagger document.. + /// Looks up a localized string similar to Reference path '{0}' does not exist in the definition section of the Swagger document.. /// - internal static string ReferenceDoesNotExist { + public static string ReferenceDoesNotExist { get { return ResourceManager.GetString("ReferenceDoesNotExist", resourceCulture); } } /// - /// Looks up a localized string similar to Swagger specification is missing title in info section. + /// Looks up a localized string similar to Swagger specification is missing title in info section. /// - internal static string TitleMissing { + public static string TitleMissing { get { return ResourceManager.GetString("TitleMissing", resourceCulture); } } /// - /// Looks up a localized string similar to Invalid swagger 2.0 specification. Missing version property. . + /// Looks up a localized string similar to Invalid swagger 2.0 specification. Missing version property. . /// - internal static string UnknownSwaggerVersion { + public static string UnknownSwaggerVersion { get { return ResourceManager.GetString("UnknownSwaggerVersion", resourceCulture); } } /// - /// Looks up a localized string similar to The operation '{0}' has a response body in response '{1}', but did not have a supported MIME type ('application/json' or 'application/octet-stream') in its Produces property.. + /// Looks up a localized string similar to The operation '{0}' has a response body in response '{1}', but did not have a supported MIME type ('application/json' or 'application/octet-stream') in its Produces property.. /// - internal static string UnsupportedMimeTypeForResponseBody { + public static string UnsupportedMimeTypeForResponseBody { get { return ResourceManager.GetString("UnsupportedMimeTypeForResponseBody", resourceCulture); } diff --git a/src/modeler/AutoRest.Swagger/Properties/Resources.resx b/src/modeler/AutoRest.Swagger/Properties/Resources.resx index 18588c7695845..2dabda057191f 100644 --- a/src/modeler/AutoRest.Swagger/Properties/Resources.resx +++ b/src/modeler/AutoRest.Swagger/Properties/Resources.resx @@ -147,6 +147,9 @@ Swagger specification is missing info section + + Input parameter is required. + The schema's '{0}' ancestors should have at lease one property diff --git a/src/modeler/AutoRest.Swagger/SwaggerModeler.cs b/src/modeler/AutoRest.Swagger/SwaggerModeler.cs index 1a4ff362c7e58..2d4b7290aa339 100644 --- a/src/modeler/AutoRest.Swagger/SwaggerModeler.cs +++ b/src/modeler/AutoRest.Swagger/SwaggerModeler.cs @@ -13,6 +13,7 @@ using AutoRest.Swagger.Model; using AutoRest.Swagger.Properties; using ParameterLocation = AutoRest.Swagger.Model.ParameterLocation; +using AutoRest.Core.Validation; namespace AutoRest.Swagger { @@ -53,19 +54,30 @@ public override string Name /// public TransferProtocolScheme DefaultProtocol { get; set; } + public override ServiceClient Build() + { + IEnumerable messages = new List(); + return Build(out messages); + } + /// /// Builds service model from swagger file. /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")] - public override ServiceClient Build() + public override ServiceClient Build(out IEnumerable messages) { Logger.LogInfo(Resources.ParsingSwagger); if (string.IsNullOrWhiteSpace(Settings.Input)) { - throw ErrorManager.CreateError("Input parameter is required."); + throw ErrorManager.CreateError(Resources.InputRequired); } ServiceDefinition = SwaggerParser.Load(Settings.Input, Settings.FileSystem); + + // Look for semantic errors and warnings in the document. + var validator = new RecursiveObjectValidator(); + messages = validator.GetValidationExceptions(ServiceDefinition).ToList(); + Logger.LogInfo(Resources.GeneratingClient); // Update settings UpdateSettings(); @@ -187,7 +199,7 @@ public virtual void InitializeClientModel() ServiceClient.Documentation = ServiceDefinition.Info.Description; if (ServiceDefinition.Schemes == null || ServiceDefinition.Schemes.Count != 1) { - ServiceDefinition.Schemes = new List {DefaultProtocol}; + ServiceDefinition.Schemes = new List { DefaultProtocol }; } if (string.IsNullOrEmpty(ServiceDefinition.Host)) { @@ -209,7 +221,7 @@ public virtual void BuildCompositeTypes() // Load any external references foreach (var reference in ServiceDefinition.ExternalReferences) { - string[] splitReference = reference.Split(new[] {'#'}, StringSplitOptions.RemoveEmptyEntries); + string[] splitReference = reference.Split(new[] { '#' }, StringSplitOptions.RemoveEmptyEntries); Debug.Assert(splitReference.Length == 2); string filePath = splitReference[0]; string externalDefinition = Settings.FileSystem.ReadFileAsText(filePath); diff --git a/src/modeler/AutoRest.Swagger/Validation/AnonymousParameterTypes.cs b/src/modeler/AutoRest.Swagger/Validation/AnonymousParameterTypes.cs new file mode 100644 index 0000000000000..cd86bed066b50 --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/AnonymousParameterTypes.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using AutoRest.Swagger.Model; + +namespace AutoRest.Swagger.Validation +{ + public class AnonymousParameterTypes : TypedRule + { + private static AnonymousTypes AnonymousTypesRule = new AnonymousTypes(); + + /// + /// An entity fails this rule if it has a schema, and that schema is an anonymous type + /// + /// + /// + public override bool IsValid(SwaggerParameter entity) => + entity == null || entity.Schema == null || AnonymousTypesRule.IsValid(entity.Schema); + + public override ValidationExceptionName Exception => ValidationExceptionName.AnonymousTypesDiscouraged; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/AnonymousTypes.cs b/src/modeler/AutoRest.Swagger/Validation/AnonymousTypes.cs new file mode 100644 index 0000000000000..f2d5322b98663 --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/AnonymousTypes.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using AutoRest.Swagger.Model; + +namespace AutoRest.Swagger.Validation +{ + public class AnonymousTypes : TypedRule + { + /// + /// An fails this rule if it doesn't have a reference (meaning it's defined inline) + /// + /// The entity to validate + /// + public override bool IsValid(SwaggerObject entity) => entity == null || !string.IsNullOrEmpty(entity.Reference); + + public override ValidationExceptionName Exception => ValidationExceptionName.AnonymousTypesDiscouraged; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/ClientNameRequired.cs b/src/modeler/AutoRest.Swagger/Validation/ClientNameRequired.cs new file mode 100644 index 0000000000000..7e7b11f561433 --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/ClientNameRequired.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using AutoRest.Swagger.Model; + +namespace AutoRest.Swagger.Validation +{ + public class ClientNameRequired : TypedRule + { + public override bool IsValid(SwaggerObject entity) + { + bool valid = true; + + object clientName = null; + if (entity != null && entity.Extensions != null && entity.Extensions.TryGetValue("x-ms-client-name", out clientName)) + { + var ext = clientName as Newtonsoft.Json.Linq.JContainer; + if (ext != null && (ext["name"] == null || string.IsNullOrEmpty(ext["name"].ToString()))) + { + valid = false; + } + else if (string.IsNullOrEmpty(clientName as string)) + { + valid = false; + } + } + + return valid; + } + + public override ValidationExceptionName Exception => ValidationExceptionName.NonEmptyClientName; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/DefaultInEnum.cs b/src/modeler/AutoRest.Swagger/Validation/DefaultInEnum.cs new file mode 100644 index 0000000000000..2e19acef64837 --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/DefaultInEnum.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using AutoRest.Swagger.Model; + +namespace AutoRest.Swagger.Validation +{ + internal static class EnumDefaultExtensions + { + /// + /// Determines if the SwaggerObject has both a default and an enum defined + /// + /// + /// + internal static bool HasDefaultAndEnum(this SwaggerObject entity) + { + return !string.IsNullOrEmpty(entity.Default) && entity.Enum != null; + } + + /// + /// Determines if the default value appears in the enum + /// + internal static bool EnumContainsDefault(this SwaggerObject entity) + { + return entity.Enum.Contains(entity.Default); + } + } + + public class EnumContainsDefault : TypedRule + { + /// + /// An fails this rule if it has both default defined and enum and the default isn't in the enum + /// + /// + /// + public override bool IsValid(SwaggerObject entity) => + entity == null || !entity.HasDefaultAndEnum() || entity.EnumContainsDefault(); + + public override ValidationExceptionName Exception => ValidationExceptionName.DefaultMustBeInEnum; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/DefaultResponseRequired.cs b/src/modeler/AutoRest.Swagger/Validation/DefaultResponseRequired.cs new file mode 100644 index 0000000000000..55f5f588f13ac --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/DefaultResponseRequired.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using AutoRest.Swagger.Model; +using System.Collections.Generic; + +namespace AutoRest.Swagger.Validation +{ + public class DefaultResponseRequired : TypedRule> + { + /// + /// This rule fails if the lacks responses or lacks a default response + /// + /// + /// + public override bool IsValid(IDictionary entity) + => entity != null && entity.ContainsKey("default"); + + public override ValidationExceptionName Exception => ValidationExceptionName.DefaultResponseRequired; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/DescriptionRequired.cs b/src/modeler/AutoRest.Swagger/Validation/DescriptionRequired.cs new file mode 100644 index 0000000000000..c27277f80a1c0 --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/DescriptionRequired.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using AutoRest.Swagger.Model; + +namespace AutoRest.Swagger.Validation +{ + public class DescriptionRequired : TypedRule + { + /// + /// This rule fails if the description is null and the reference is null (since the reference could have a description) + /// + /// + /// + public override bool IsValid(SwaggerObject entity) + => entity == null || entity.Description != null || string.IsNullOrEmpty(entity.Reference); + + public override ValidationExceptionName Exception => ValidationExceptionName.DescriptionRequired; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/DescriptiveDescriptionRequired.cs b/src/modeler/AutoRest.Swagger/Validation/DescriptiveDescriptionRequired.cs new file mode 100644 index 0000000000000..4011d7ea95b7f --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/DescriptiveDescriptionRequired.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace AutoRest.Swagger.Validation +{ + internal static class DescriptiveDescriptionsExtensions + { + private static IEnumerable ImpermissibleDescriptions = new List() + { + "description" + }; + + /// + /// Determines if the string is a value that is not allowed (case insensitive) + /// + internal static bool IsImpermissibleValue(this string description) + { + return ImpermissibleDescriptions.Any(s => s.Equals(description, System.StringComparison.InvariantCultureIgnoreCase)); + } + } + + public class DescriptiveDescriptionRequired : TypedRule + { + /// + /// This test passes if the is not just empty or whitespace and not explictly blocked + /// + /// + /// + public override bool IsValid(string description) + => !string.IsNullOrWhiteSpace(description) && !description.IsImpermissibleValue(); + + public override ValidationExceptionName Exception => ValidationExceptionName.DescriptiveDescription; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/OperationIdNounInVerb.cs b/src/modeler/AutoRest.Swagger/Validation/OperationIdNounInVerb.cs new file mode 100644 index 0000000000000..79e6c95542ce4 --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/OperationIdNounInVerb.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace AutoRest.Swagger.Validation +{ + public class OperationIdNounInVerb : TypedRule + { + private const string NOUN_VERB_PATTERN = "^(\\w+)?_(\\w+)$"; + + /// + /// This rule passes if the operation id doesn't contain a repeated value before and after the underscore + /// e.g. User_GetUser + /// or Users_DeleteUser + /// or User_ListUsers + /// + /// The operation id to test + /// The noun to be put in the failure message + /// + public override bool IsValid(string entity, out object[] formatParameters) + { + foreach (Match match in Regex.Matches(entity, NOUN_VERB_PATTERN)) + { + if (match.Groups.Count != 3) + { + // If we don't have 3 groups, then the regex has been changed from capturing [{Match}, {Noun}, {Verb}]. + throw new InvalidOperationException("Regex pattern does not conform to Noun_Verb pattern"); + } + + // Get the noun and verb parts of the operation id + var noun = match.Groups[1].Value; + var verb = match.Groups[2].Value; + + // The noun is sometimes singlular or plural, but we want to catch the other versions in the verb as well + var nounSearchPattern = noun + (noun.Last() == 's' ? "?" : string.Empty); + if (Regex.IsMatch(verb, nounSearchPattern)) + { + formatParameters = new string[] { noun }; + return false; + } + } + formatParameters = new object[] {}; + return true; + } + + public override ValidationExceptionName Exception => ValidationExceptionName.OperationIdNounsNotInVerbs; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/OperationIdSingleUnderscore.cs b/src/modeler/AutoRest.Swagger/Validation/OperationIdSingleUnderscore.cs new file mode 100644 index 0000000000000..a943086ccdfd9 --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/OperationIdSingleUnderscore.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Linq; +using AutoRest.Core.Validation; + +namespace AutoRest.Swagger.Validation +{ + public class OperationIdSingleUnderscore : TypedRule + { + /// + /// This rule passes if the entity contains no more than 1 underscore + /// + /// + /// + public override bool IsValid(string entity) + => entity != null && entity.Count(c => c == '_') <= 1; + + public override ValidationExceptionName Exception => ValidationExceptionName.OneUnderscoreInOperationId; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/RefNoSiblings.cs b/src/modeler/AutoRest.Swagger/Validation/RefNoSiblings.cs new file mode 100644 index 0000000000000..eb5b388de135e --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/RefNoSiblings.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using AutoRest.Swagger.Model; + +namespace AutoRest.Swagger.Validation +{ + internal static class SwaggerObjectExtensions + { + internal static bool DefinesInlineProperties(this SwaggerObject entity) + { + return entity.Description != null + || entity.Items != null + || entity.Type != null; + } + } + + public class RefNoSiblings : TypedRule + { + /// + /// This rule passes if the entity does not have both a reference and define properties inline + /// + /// + /// + public override bool IsValid(SwaggerObject entity) + => entity == null || string.IsNullOrEmpty(entity.Reference) || !entity.DefinesInlineProperties(); + + public override ValidationExceptionName Exception => ValidationExceptionName.RefsMustNotHaveSiblings; + } +} diff --git a/src/modeler/AutoRest.Swagger/Validation/XmsPathsInPath.cs b/src/modeler/AutoRest.Swagger/Validation/XmsPathsInPath.cs new file mode 100644 index 0000000000000..9f6f1fa185e0b --- /dev/null +++ b/src/modeler/AutoRest.Swagger/Validation/XmsPathsInPath.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AutoRest.Core.Validation; +using AutoRest.Swagger.Model; +using System.Collections.Generic; +using System.Linq; + +namespace AutoRest.Swagger.Validation +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xms")] + public class XmsPathsInPath : TypedRule + { + public override IEnumerable GetValidationMessages(ServiceDefinition entity) + { + return entity?.CustomPaths?.Keys + .Where(customPath => !entity.Paths.ContainsKey(GetBasePath(customPath))) + .Select(basePath => CreateException(Exception, basePath)) + + ?? Enumerable.Empty(); + } + + private static string GetBasePath(string customPath) + { + var index = customPath.IndexOf('?'); + if (index == -1) + { + return customPath; + } + return customPath.Substring(0, index); + } + + public override ValidationExceptionName Exception => ValidationExceptionName.XmsPathsMustOverloadPaths; + } +}