diff --git a/src/Tools/Extensions.ApiDescription.Client/src/CSharpIdentifier.cs b/src/Tools/Extensions.ApiDescription.Client/src/CSharpIdentifier.cs index b1ee4f7c0c..c7d2d13e49 100644 --- a/src/Tools/Extensions.ApiDescription.Client/src/CSharpIdentifier.cs +++ b/src/Tools/Extensions.ApiDescription.Client/src/CSharpIdentifier.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Globalization; diff --git a/src/Tools/Extensions.ApiDescription.Client/src/GetOpenApiReferenceMetadata.cs b/src/Tools/Extensions.ApiDescription.Client/src/GetOpenApiReferenceMetadata.cs index e9fe7b53a3..b7894e0bf0 100644 --- a/src/Tools/Extensions.ApiDescription.Client/src/GetOpenApiReferenceMetadata.cs +++ b/src/Tools/Extensions.ApiDescription.Client/src/GetOpenApiReferenceMetadata.cs @@ -63,8 +63,7 @@ namespace Microsoft.Extensions.ApiDescription.Client "OpenApiReference" : "OpenApiProjectReference"; - Log.LogError( - Resources.FormatInvalidEmptyMetadataValue("CodeGenerator", "OpenApiReference", item.ItemSpec)); + Log.LogError(Resources.FormatInvalidEmptyMetadataValue("CodeGenerator", type, item.ItemSpec)); continue; } diff --git a/src/Tools/Extensions.ApiDescription.Client/src/Properties/AssemblyInfo.cs b/src/Tools/Extensions.ApiDescription.Client/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..5d2f75d233 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Extensions.ApiDescription.Client.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Tools/Extensions.ApiDescription.Client/src/build/Microsoft.Extensions.ApiDescription.Client.targets b/src/Tools/Extensions.ApiDescription.Client/src/build/Microsoft.Extensions.ApiDescription.Client.targets index c1f5eee677..af590565c6 100644 --- a/src/Tools/Extensions.ApiDescription.Client/src/build/Microsoft.Extensions.ApiDescription.Client.targets +++ b/src/Tools/Extensions.ApiDescription.Client/src/build/Microsoft.Extensions.ApiDescription.Client.targets @@ -48,7 +48,9 @@ - + + %(_Temporary.OriginalItemSpec) + <_Temporary Remove="@(_Temporary)" /> diff --git a/src/Tools/Extensions.ApiDescription.Client/test/CSharpIdentifierTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/CSharpIdentifierTest.cs new file mode 100644 index 0000000000..d99b3a55d6 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/CSharpIdentifierTest.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.Extensions.ApiDescription.Client +{ + public class CSharpIdentifierTest + { + [Theory] + [InlineData('a')] + [InlineData('Q')] + [InlineData('\u2164')] // UnicodeCategory.LetterNumber (roman numeral five) + [InlineData('_')] + [InlineData('9')] + [InlineData('\u0303')] // UnicodeCategory.NonSpacingMark (combining tilde) + [InlineData('\u09CB')] // UnicodeCategory.SpacingCombiningMark (Bengali vowel sign O) + [InlineData('\uFE4F')] // UnicodeCategory.ConnectorPunctuation (wavy low line) + [InlineData('\u2062')] // UnicodeCategory.Format (invisible times) + public void IsIdentifierPart_ReturnsTrue_WhenItShould(char character) + { + // Arrange and Act + var result = CSharpIdentifier.IsIdentifierPart(character); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData('/')] + [InlineData('-')] + [InlineData('\u20DF')] // UnicodeCategory.EnclosingMark (combining enclosing diamond) + [InlineData('\u2005')] // UnicodeCategory.SpaceSeparator (four-per-em space) + [InlineData('\u0096')] // UnicodeCategory.Control (start of guarded area) + [InlineData('\uFF1C')] // UnicodeCategory.MathSymbol (fullwidth less-than sign) + public void IsIdentifierPart_ReturnsFalse_WhenItShould(char character) + { + // Arrange and Act + var result = CSharpIdentifier.IsIdentifierPart(character); + + // Assert + Assert.False(result); + } + + // Output length is one longer than input in these cases. + [Theory] + [InlineData("9", "_9")] + [InlineData("\u0303", "_\u0303")] // UnicodeCategory.NonSpacingMark (combining tilde) + [InlineData("\u09CB", "_\u09CB")] // UnicodeCategory.SpacingCombiningMark (Bengali vowel sign O) + [InlineData("\uFE4F", "_\uFE4F")] // UnicodeCategory.ConnectorPunctuation (wavy low line) + [InlineData("\u2062", "_\u2062")] // UnicodeCategory.Format (invisible times) + public void SanitizeIdentifier_AddsUnderscore_WhenItShould(string input, string expectdOutput) + { + // Arrange and Act + var output = CSharpIdentifier.SanitizeIdentifier(input); + + // Assert + Assert.Equal(expectdOutput, output); + } + + [Theory] + [InlineData("a", "a")] + [InlineData("Q", "Q")] + [InlineData("\u2164", "\u2164")] + [InlineData("_", "_")] + public void SanitizeIdentifier_DoesNotAddUnderscore_WhenValidStartCharacter(string input, string expectdOutput) + { + // Arrange and Act + var output = CSharpIdentifier.SanitizeIdentifier(input); + + // Assert + Assert.Equal(expectdOutput, output); + } + + [Theory] + [InlineData("/", "_")] + [InlineData("-", "_")] + [InlineData("\u20DF", "_")] // UnicodeCategory.EnclosingMark (combining enclosing diamond) + [InlineData("\u2005", "_")] // UnicodeCategory.SpaceSeparator (four-per-em space) + [InlineData("\u0096", "_")] // UnicodeCategory.Control (start of guarded area) + [InlineData("\uFF1C", "_")] // UnicodeCategory.MathSymbol (fullwidth less-than sign) + public void SanitizeIdentifier_DoesNotAddUnderscore_WhenInvalidCharacter(string input, string expectdOutput) + { + // Arrange and Act + var output = CSharpIdentifier.SanitizeIdentifier(input); + + // Assert + Assert.Equal(expectdOutput, output); + } + + [Theory] + [InlineData("a/", "a_")] + [InlineData("aa-bb", "aa_bb")] + [InlineData("aa\u20DF\u20DF", "aa__")] // UnicodeCategory.EnclosingMark (combining enclosing diamond) + [InlineData("aa\u2005bb\u2005cc", "aa_bb_cc")] // UnicodeCategory.SpaceSeparator (four-per-em space) + [InlineData("aa\u0096\u0096bb", "aa__bb")] // UnicodeCategory.Control (start of guarded area) + [InlineData("aa\uFF1C\uFF1C\uFF1Cbb", "aa___bb")] // UnicodeCategory.MathSymbol (fullwidth less-than sign) + public void SanitizeIdentifier_ReplacesInvalidCharacters_WhenNotFirst(string input, string expectdOutput) + { + // Arrange and Act + var output = CSharpIdentifier.SanitizeIdentifier(input); + + // Assert + Assert.Equal(expectdOutput, output); + } + } +} diff --git a/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs new file mode 100644 index 0000000000..1b6abc9f27 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/GetCurrentOpenApiReferenceTest.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.ApiDescription.Client +{ + public class GetCurrentOpenApiReferenceTest + { + [Fact] + public void Execute_ReturnsExpectedItem() + { + // Arrange + string input = "Identity=../files/azureMonitor.json|ClassName=azureMonitorClient|" + + "CodeGenerator=NSwagCSharp|Namespace=ConsoleClient|Options=|OutputPath=" + + "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs|" + + "OriginalItemSpec=../files/azureMonitor.json|FirstForGenerator=true"; + var task = new GetCurrentOpenApiReference + { + Input = input, + }; + + string expectedIdentity = "../files/azureMonitor.json"; + IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "azureMonitorClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", "ConsoleClient" }, + { "Options", "" }, + { "OriginalItemSpec", expectedIdentity }, + { "OutputPath", "C:\\dd\\dnx\\AspNetCore\\artifacts\\obj\\ConsoleClient\\azureMonitorClient.cs" }, + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.False(task.Log.HasLoggedErrors); + var output = Assert.Single(task.Outputs); + Assert.Equal(expectedIdentity, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + } + } +} diff --git a/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs b/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs new file mode 100644 index 0000000000..7bc5cc7fb9 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/GetOpenApiReferenceMetadataTest.cs @@ -0,0 +1,576 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Utilities; +using Xunit; + +namespace Microsoft.Extensions.ApiDescription.Client +{ + public class GetOpenApiReferenceMetadataTest + { + [Fact] + public void Execute_AddsExpectedMetadata() + { + // Arrange + var identity = Path.Combine("TestProjects", "files", "NSwag.json"); + var @namespace = "Console.Client"; + var outputPath = Path.Combine("obj", "NSwagClient.cs"); + var inputMetadata = new Dictionary { { "CodeGenerator", "NSwagCSharp" } }; + var task = new GetOpenApiReferenceMetadata + { + Extension = ".cs", + Inputs = new[] { new TaskItem(identity, inputMetadata) }, + Namespace = @namespace, + OutputDirectory = "obj", + }; + + IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "NSwagClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity }, + { "OutputPath", outputPath }, + { + "SerializedMetadata", + $"Identity={identity}|CodeGenerator=NSwagCSharp|" + + $"OriginalItemSpec={identity}|FirstForGenerator=true|" + + $"OutputPath={outputPath}|ClassName=NSwagClient|Namespace={@namespace}" + }, + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.False(task.Log.HasLoggedErrors); + var output = Assert.Single(task.Outputs); + Assert.Equal(identity, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + } + + [Fact] + public void Execute_DoesNotOverrideClassName() + { + // Arrange + var identity = Path.Combine("TestProjects", "files", "NSwag.json"); + var className = "ThisIsClassy"; + var @namespace = "Console.Client"; + var outputPath = Path.Combine("obj", $"NSwagClient.cs"); + var inputMetadata = new Dictionary + { + { "CodeGenerator", "NSwagCSharp" }, + { "ClassName", className }, + }; + + var task = new GetOpenApiReferenceMetadata + { + Extension = ".cs", + Inputs = new[] { new TaskItem(identity, inputMetadata) }, + Namespace = @namespace, + OutputDirectory = "obj", + }; + + IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", className }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity }, + { "OutputPath", outputPath }, + { + "SerializedMetadata", + $"Identity={identity}|CodeGenerator=NSwagCSharp|" + + $"ClassName={className}|OriginalItemSpec={identity}|FirstForGenerator=true|" + + $"OutputPath={outputPath}|Namespace={@namespace}" + }, + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.False(task.Log.HasLoggedErrors); + var output = Assert.Single(task.Outputs); + Assert.Equal(identity, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + } + + [Fact] + public void Execute_DoesNotOverrideNamespace() + { + // Arrange + var defaultNamespace = "Console.Client"; + var identity = Path.Combine("TestProjects", "files", "NSwag.json"); + var @namespace = "NotConsole.NotClient"; + var outputPath = Path.Combine("obj", "NSwagClient.cs"); + var inputMetadata = new Dictionary + { + { "CodeGenerator", "NSwagCSharp" }, + { "Namespace", @namespace }, + }; + + var task = new GetOpenApiReferenceMetadata + { + Extension = ".cs", + Inputs = new[] { new TaskItem(identity, inputMetadata) }, + Namespace = defaultNamespace, + OutputDirectory = "obj", + }; + + IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", "NSwagClient" }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity }, + { "OutputPath", outputPath }, + { + "SerializedMetadata", + $"Identity={identity}|CodeGenerator=NSwagCSharp|" + + $"Namespace={@namespace}|OriginalItemSpec={identity}|FirstForGenerator=true|" + + $"OutputPath={outputPath}|ClassName=NSwagClient" + }, + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.False(task.Log.HasLoggedErrors); + var output = Assert.Single(task.Outputs); + Assert.Equal(identity, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + } + + [Fact] + public void Execute_DoesNotOverrideOutputPath_IfRooted() + { + // Arrange + var identity = Path.Combine("TestProjects", "files", "NSwag.json"); + var className = "ThisIsClassy"; + var @namespace = "Console.Client"; + var outputPath = Path.Combine(Path.GetTempPath(), $"{className}.cs"); + var inputMetadata = new Dictionary + { + { "CodeGenerator", "NSwagCSharp" }, + { "OutputPath", outputPath } + }; + + var task = new GetOpenApiReferenceMetadata + { + Extension = ".cs", + Inputs = new[] { new TaskItem(identity, inputMetadata) }, + Namespace = @namespace, + OutputDirectory = "bin", + }; + + IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", className }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity }, + { "OutputPath", outputPath }, + { + "SerializedMetadata", + $"Identity={identity}|CodeGenerator=NSwagCSharp|" + + $"OutputPath={outputPath}|OriginalItemSpec={identity}|FirstForGenerator=true|" + + $"ClassName={className}|Namespace={@namespace}" + }, + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.False(task.Log.HasLoggedErrors); + var output = Assert.Single(task.Outputs); + Assert.Equal(identity, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + } + + [Fact] + public void Execute_LogsError_IfCodeGeneratorMissing() + { + // Arrange + var identity1 = Path.Combine("TestProjects", "files", "NSwag.json"); + var identity2 = Path.Combine("TestProjects", "files", "swashbuckle.json"); + var error1 = Resources.FormatInvalidEmptyMetadataValue("CodeGenerator", "OpenApiReference", identity1); + var error2 = Resources.FormatInvalidEmptyMetadataValue("CodeGenerator", "OpenApiProjectReference", identity2); + var @namespace = "Console.Client"; + var inputMetadata1 = new Dictionary + { + { "ExtraMetadata", "this is extra" }, + }; + var inputMetadata2 = new Dictionary + { + { "Options", "-quiet" }, + { "SourceProject", "ConsoleProject.csproj" }, + }; + + var buildEngine = new MockBuildEngine(); + var task = new GetOpenApiReferenceMetadata + { + BuildEngine = buildEngine, + Extension = ".cs", + Inputs = new[] + { + new TaskItem(identity1, inputMetadata1), + new TaskItem(identity2, inputMetadata2), + }, + Namespace = @namespace, + OutputDirectory = "obj", + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.False(result); + Assert.True(task.Log.HasLoggedErrors); + Assert.Equal(2, buildEngine.Errors); + Assert.Equal(0, buildEngine.Messages); + Assert.Equal(0, buildEngine.Warnings); + Assert.Contains(error1, buildEngine.Log, StringComparison.OrdinalIgnoreCase); + Assert.Contains(error2, buildEngine.Log, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Execute_LogsError_IfOutputPathDuplicated() + { + // Arrange + var identity = Path.Combine("TestProjects", "files", "NSwag.json"); + var codeGenerator = "NSwagCSharp"; + var error = Resources.FormatDuplicateFileOutputPaths(Path.Combine("obj", "NSwagClient.cs")); + var @namespace = "Console.Client"; + var inputMetadata1 = new Dictionary + { + { "CodeGenerator", codeGenerator }, + { "ExtraMetadata", "this is extra" }, + }; + var inputMetadata2 = new Dictionary + { + { "CodeGenerator", codeGenerator }, + { "Options", "-quiet" }, + }; + + var buildEngine = new MockBuildEngine(); + var task = new GetOpenApiReferenceMetadata + { + BuildEngine = buildEngine, + Extension = ".cs", + Inputs = new[] + { + new TaskItem(identity, inputMetadata1), + new TaskItem(identity, inputMetadata2), + }, + Namespace = @namespace, + OutputDirectory = "obj", + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.False(result); + Assert.True(task.Log.HasLoggedErrors); + Assert.Equal(1, buildEngine.Errors); + Assert.Equal(0, buildEngine.Messages); + Assert.Equal(0, buildEngine.Warnings); + Assert.Contains(error, buildEngine.Log, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Execute_SetsClassName_BasedOnOutputPath() + { + // Arrange + var identity = Path.Combine("TestProjects", "files", "NSwag.json"); + var className = "ThisIsClassy"; + var @namespace = "Console.Client"; + var outputPath = $"{className}.cs"; + var expectedOutputPath = Path.Combine("bin", outputPath); + var inputMetadata = new Dictionary + { + { "CodeGenerator", "NSwagCSharp" }, + { "OutputPath", outputPath } + }; + + var task = new GetOpenApiReferenceMetadata + { + Extension = ".cs", + Inputs = new[] { new TaskItem(identity, inputMetadata) }, + Namespace = @namespace, + OutputDirectory = "bin", + }; + + IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", className }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity }, + { "OutputPath", expectedOutputPath }, + { + "SerializedMetadata", + $"Identity={identity}|CodeGenerator=NSwagCSharp|" + + $"OutputPath={expectedOutputPath}|OriginalItemSpec={identity}|FirstForGenerator=true|" + + $"ClassName={className}|Namespace={@namespace}" + }, + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.False(task.Log.HasLoggedErrors); + var output = Assert.Single(task.Outputs); + Assert.Equal(identity, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + } + + [Theory] + [InlineData("aa-bb.cs", "aa_bb")] + [InlineData("aa.bb.cc.ts", "aa_bb_cc")] + [InlineData("aa\u20DF\u20DF.tsx", "aa__")] // UnicodeCategory.EnclosingMark (combining enclosing diamond) + [InlineData("aa\u2005bb\u2005cc.cs", "aa_bb_cc")] // UnicodeCategory.SpaceSeparator (four-per-em space) + [InlineData("aa\u0096\u0096bb.cs", "aa__bb")] // UnicodeCategory.Control (start of guarded area) + [InlineData("aa\uFF1C\uFF1C\uFF1Cbb.cs", "aa___bb")] // UnicodeCategory.MathSymbol (fullwidth less-than sign) + public void Execute_SetsClassName_BasedOnSanitizedOutputPath(string outputPath, string className) + { + // Arrange + var identity = Path.Combine("TestProjects", "files", "NSwag.json"); + var @namespace = "Console.Client"; + var expectedOutputPath = Path.Combine("bin", outputPath); + var inputMetadata = new Dictionary + { + { "CodeGenerator", "NSwagCSharp" }, + { "OutputPath", outputPath } + }; + + var task = new GetOpenApiReferenceMetadata + { + Extension = ".cs", + Inputs = new[] { new TaskItem(identity, inputMetadata) }, + Namespace = @namespace, + OutputDirectory = "bin", + }; + + IDictionary expectedMetadata = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", className }, + { "CodeGenerator", "NSwagCSharp" }, + { "FirstForGenerator", "true" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity }, + { "OutputPath", expectedOutputPath }, + { + "SerializedMetadata", + $"Identity={identity}|CodeGenerator=NSwagCSharp|" + + $"OutputPath={expectedOutputPath}|OriginalItemSpec={identity}|FirstForGenerator=true|" + + $"ClassName={className}|Namespace={@namespace}" + }, + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.False(task.Log.HasLoggedErrors); + var output = Assert.Single(task.Outputs); + Assert.Equal(identity, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + + // The dictionary CloneCustomMetadata returns doesn't provide a useful KeyValuePair enumerator. + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata, orderedMetadata); + } + + [Fact] + public void Execute_SetsFirstForGenerator_UsesCorrectExtension() + { + // Arrange + var identity12 = Path.Combine("TestProjects", "files", "NSwag.json"); + var identity3 = Path.Combine("TestProjects", "files", "swashbuckle.json"); + var className12 = "NSwagClient"; + var className3 = "swashbuckleClient"; + var codeGenerator13 = "NSwagCSharp"; + var codeGenerator2 = "NSwagTypeScript"; + var inputMetadata1 = new Dictionary { { "CodeGenerator", codeGenerator13 } }; + var inputMetadata2 = new Dictionary { { "CodeGenerator", codeGenerator2 } }; + var inputMetadata3 = new Dictionary { { "CodeGenerator", codeGenerator13 } }; + var @namespace = "Console.Client"; + var outputPath1 = Path.Combine("obj", $"{className12}.cs"); + var outputPath2 = Path.Combine("obj", $"{className12}.ts"); + var outputPath3 = Path.Combine("obj", $"{className3}.cs"); + + var task = new GetOpenApiReferenceMetadata + { + Extension = ".cs", + Inputs = new[] + { + new TaskItem(identity12, inputMetadata1), + new TaskItem(identity12, inputMetadata2), + new TaskItem(identity3, inputMetadata3), + }, + Namespace = @namespace, + OutputDirectory = "obj", + }; + + IDictionary expectedMetadata1 = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", className12 }, + { "CodeGenerator", codeGenerator13 }, + { "FirstForGenerator", "true" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity12 }, + { "OutputPath", outputPath1 }, + { + "SerializedMetadata", + $"Identity={identity12}|CodeGenerator={codeGenerator13}|" + + $"OriginalItemSpec={identity12}|FirstForGenerator=true|" + + $"OutputPath={outputPath1}|ClassName={className12}|Namespace={@namespace}" + }, + }; + IDictionary expectedMetadata2 = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", className12 }, + { "CodeGenerator", codeGenerator2 }, + { "FirstForGenerator", "true" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity12 }, + { "OutputPath", outputPath2 }, + { + "SerializedMetadata", + $"Identity={identity12}|CodeGenerator={codeGenerator2}|" + + $"OriginalItemSpec={identity12}|FirstForGenerator=true|" + + $"OutputPath={outputPath2}|ClassName={className12}|Namespace={@namespace}" + }, + }; + IDictionary expectedMetadata3 = new SortedDictionary(StringComparer.Ordinal) + { + { "ClassName", className3 }, + { "CodeGenerator", codeGenerator13 }, + { "FirstForGenerator", "false" }, + { "Namespace", @namespace }, + { "OriginalItemSpec", identity3 }, + { "OutputPath", outputPath3 }, + { + "SerializedMetadata", + $"Identity={identity3}|CodeGenerator={codeGenerator13}|" + + $"OriginalItemSpec={identity3}|FirstForGenerator=false|" + + $"OutputPath={outputPath3}|ClassName={className3}|Namespace={@namespace}" + }, + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.False(task.Log.HasLoggedErrors); + Assert.Collection( + task.Outputs, + output => + { + Assert.Equal(identity12, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata1, orderedMetadata); + }, + output => + { + Assert.Equal(identity12, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata2, orderedMetadata); + }, + output => + { + Assert.Equal(identity3, output.ItemSpec); + var metadata = Assert.IsAssignableFrom>(output.CloneCustomMetadata()); + var orderedMetadata = new SortedDictionary(StringComparer.Ordinal); + foreach (var key in metadata.Keys) + { + orderedMetadata.Add(key, metadata[key]); + } + + Assert.Equal(expectedMetadata3, orderedMetadata); + }); + } + } +} diff --git a/src/Tools/Extensions.ApiDescription.Client/test/Microsoft.Extensions.ApiDescription.Client.Tests.csproj b/src/Tools/Extensions.ApiDescription.Client/test/Microsoft.Extensions.ApiDescription.Client.Tests.csproj new file mode 100644 index 0000000000..c6a5144e61 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/Microsoft.Extensions.ApiDescription.Client.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.0 + $(DefaultItemExcludes);TestProjects\**\* + ApiDescriptionClientTests + + + + + + + + + + + + + + + diff --git a/src/Tools/Extensions.ApiDescription.Client/test/MockBuildEngine.cs b/src/Tools/Extensions.ApiDescription.Client/test/MockBuildEngine.cs new file mode 100644 index 0000000000..a78d71ef5b --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/MockBuildEngine.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.Build.Framework; + +// Inspired by https://github.com/microsoft/msbuild/blob/master/src/Utilities.UnitTests/MockEngine.cs +namespace Microsoft.Extensions.ApiDescription.Client +{ + internal sealed class MockBuildEngine : IBuildEngine3 + { + private readonly StringBuilder _log = new StringBuilder(); + + public bool IsRunningMultipleNodes => false; + + public bool ContinueOnError => false; + + public string ProjectFileOfTaskNode => string.Empty; + + public int LineNumberOfTaskNode => 0; + + public int ColumnNumberOfTaskNode => 0; + + internal MessageImportance MinimumMessageImportance { get; set; } = MessageImportance.Low; + + internal int Messages { set; get; } + + internal int Warnings { set; get; } + + internal int Errors { set; get; } + + internal string Log => _log.ToString(); + + public bool BuildProjectFile( + string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs) => false; + + public bool BuildProjectFile( + string projectFileName, + string[] targetNames, + IDictionary globalProperties, + IDictionary targetOutputs, + string toolsVersion) => false; + + public bool BuildProjectFilesInParallel( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IDictionary[] targetOutputsPerProject, + string[] toolsVersion, + bool useResultsCache, + bool unloadProjectsOnCompletion) => false; + + public BuildEngineResult BuildProjectFilesInParallel( + string[] projectFileNames, + string[] targetNames, + IDictionary[] globalProperties, + IList[] undefineProperties, + string[] toolsVersion, + bool includeTargetOutputs) => new BuildEngineResult(false, null); + + public void LogErrorEvent(BuildErrorEventArgs eventArgs) + { + _log.AppendLine(eventArgs.Message); + Errors++; + } + + public void LogWarningEvent(BuildWarningEventArgs eventArgs) + { + _log.AppendLine(eventArgs.Message); + Warnings++; + } + + public void LogCustomEvent(CustomBuildEventArgs eventArgs) + { + _log.AppendLine(eventArgs.Message); + } + + public void LogMessageEvent(BuildMessageEventArgs eventArgs) + { + // Only record the message if it is above the minimum importance. MessageImportance enum has higher values + // for lower importance. + if (eventArgs.Importance <= MinimumMessageImportance) + { + _log.AppendLine(eventArgs.Message); + Messages++; + } + } + + public void Reacquire() + { + } + + public void Yield() + { + } + } +} diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/ConsoleClient.csproj b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/ConsoleClient.csproj new file mode 100644 index 0000000000..642c7353b6 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/ConsoleClient.csproj @@ -0,0 +1,38 @@ + + + + + Exe + netcoreapp3.0 + true + + + + + + + + + + + + <_Files Include="../../Microsoft.Extensions.ApiDescription.Client.*" + Exclude="../../Microsoft.Extensions.ApiDescription.Client.Tests.*" /> + + + + + + + + diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/Program.cs b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/Program.cs new file mode 100644 index 0000000000..9e223fb7c0 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/ConsoleClient/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace ConsoleClient +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/build/Fakes.targets b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/build/Fakes.targets new file mode 100644 index 0000000000..78597f2ac6 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/build/Fakes.targets @@ -0,0 +1,40 @@ + + + + + + + <_Metadata>'%(CurrentOpenApiReference.FullPath)' + <_Metadata>$(_Metadata) Class: '%(CurrentOpenApiReference.Namespace).%(CurrentOpenApiReference.ClassName)' + <_Metadata>$(_Metadata) FirstForGenerator: '%(CurrentOpenApiReference.FirstForGenerator)' + <_Metadata>$(_Metadata) Options: '%(CurrentOpenApiReference.Options)' + <_Metadata>$(_Metadata) OutputPath: '%(CurrentOpenApiReference.OutputPath)' + + + + + + + + + + <_Metadata>'%(FullPath)' Class: '%(Namespace).%(ClassName)' + <_Metadata>$(_Metadata) FirstForGenerator: '%(FirstForGenerator)' Options: '%(Options)' + <_Metadata>$(_Metadata) OutputPath: '%(OutputPath)' + + + + + + + + + + <_Metadata>'%(FullPath)' Class: '%(Namespace).%(ClassName)' + <_Metadata>$(_Metadata) FirstForGenerator: '%(FirstForGenerator)' Options: '%(Options)' + <_Metadata>$(_Metadata) OutputPath: '%(OutputPath)' + + + + + diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/NSwag.json b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/NSwag.json new file mode 100644 index 0000000000..f8ba05b6f7 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/NSwag.json @@ -0,0 +1,183 @@ +{ + "x-generator": "NSwag v11.18.6.0 (NJsonSchema v9.10.67.0 (Newtonsoft.Json v11.0.0.0))", + "swagger": "2.0", + "info": { + "title": "My Title", + "version": "1.0.0" + }, + "host": "localhost:5000", + "schemes": [ + "http" + ], + "consumes": [ + "application/json-patch+json", + "application/json", + "text/json", + "application/*+json", + "application/xml", + "text/xml", + "application/*+xml" + ], + "produces": [ + "text/plain", + "application/json", + "text/json", + "application/xml", + "text/xml" + ], + "paths": { + "/api/Values/matches": { + "post": { + "tags": [ + "Values" + ], + "operationId": "Values_Matches", + "consumes": [ + "application/json-patch+json", + "application/json", + "text/json", + "application/*+json", + "application/xml", + "text/xml", + "application/*+xml" + ], + "parameters": [ + { + "name": "possibilities", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "x-nullable": true + }, + { + "type": "integer", + "name": "comparisonType", + "in": "query", + "x-schema": { + "$ref": "#/definitions/StringComparison" + }, + "x-nullable": false, + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + } + ], + "responses": { + "200": { + "x-nullable": true, + "description": "", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/api/Values": { + "get": { + "tags": [ + "Values" + ], + "operationId": "Values_Get", + "responses": { + "200": { + "x-nullable": true, + "description": "", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "post": { + "tags": [ + "Values" + ], + "operationId": "Values_Post", + "consumes": [ + "application/xml" + ], + "parameters": [ + { + "name": "value", + "in": "body", + "required": true, + "schema": { + "type": "string" + }, + "x-nullable": true + } + ], + "responses": { + "200": { + "description": "" + } + } + } + }, + "/api/Values/too": { + "get": { + "tags": [ + "Values" + ], + "operationId": "Values_GetToo", + "responses": { + "200": { + "x-nullable": true, + "description": "", + "schema": { + "$ref": "#/definitions/Model" + } + } + } + } + } + }, + "definitions": { + "StringComparison": { + "type": "integer", + "description": "", + "x-enumNames": [ + "CurrentCulture", + "CurrentCultureIgnoreCase", + "InvariantCulture", + "InvariantCultureIgnoreCase", + "Ordinal", + "OrdinalIgnoreCase" + ], + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + "Model": { + "type": "object", + "additionalProperties": false, + "properties": { + "userName": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/azureMonitor.json b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/azureMonitor.json new file mode 100644 index 0000000000..a63e1f5f2a --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/azureMonitor.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"Microsoft Insights API","version":"2018-04-16","description":"Azure Monitor client to create/update/delete Scheduled Query Rules"},"host":"management.azure.com","schemes":["https"],"consumes":["application/json"],"produces":["application/json"],"security":[{"azure_auth":["user_impersonation"]}],"securityDefinitions":{"azure_auth":{"type":"oauth2","authorizationUrl":"https://login.microsoftonline.com/common/oauth2/authorize","flow":"implicit","description":"Azure Active Directory OAuth2 Flow","scopes":{"user_impersonation":"impersonate your user account"}}},"paths":{"/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.insights/scheduledQueryRules/{ruleName}":{"put":{"description":"Creates or updates an log search rule.","tags":["scheduledQueryRules"],"operationId":"ScheduledQueryRules_CreateOrUpdate","parameters":[{"$ref":"#/parameters/SubscriptionIdParameter"},{"$ref":"#/parameters/ResourceGroupNameParameter"},{"$ref":"#/parameters/RuleNameParameter"},{"$ref":"#/parameters/ApiVersionParameter"},{"name":"parameters","in":"body","required":true,"schema":{"$ref":"#/definitions/LogSearchRuleResource"},"description":"The parameters of the rule to create or update."}],"responses":{"default":{"description":"Error response describing why the operation failed.","schema":{"$ref":"#/definitions/ErrorResponse"}},"200":{"description":"Successful request to update an Log Search rule","schema":{"$ref":"#/definitions/LogSearchRuleResource"}},"201":{"description":"Created alert rule","schema":{"$ref":"#/definitions/LogSearchRuleResource"}}},"x-ms-examples":{"Create or Update rule - AletringAction":{"$ref":"./examples/createOrUpdateScheduledQueryRules.json"},"Create or Update rule - LogToMetricAction":{"$ref":"./examples/createOrUpdateScheduledQueryRule-LogToMetricAction.json"}}},"get":{"description":"Gets an Log Search rule","tags":["scheduledQueryRules"],"operationId":"ScheduledQueryRules_Get","parameters":[{"$ref":"#/parameters/ResourceGroupNameParameter"},{"$ref":"#/parameters/RuleNameParameter"},{"$ref":"#/parameters/ApiVersionParameter"},{"$ref":"#/parameters/SubscriptionIdParameter"}],"responses":{"default":{"description":"Error response describing why the operation failed.","schema":{"$ref":"#/definitions/ErrorResponse"}},"200":{"description":"Successful request to get a Log Search rule","schema":{"$ref":"#/definitions/LogSearchRuleResource"}}},"x-ms-examples":{"Get rule":{"$ref":"./examples/getScheduledQueryRules.json"}}},"patch":{"tags":["scheduledQueryRules"],"description":"Update log search Rule.","operationId":"ScheduledQueryRules_Update","parameters":[{"$ref":"#/parameters/SubscriptionIdParameter"},{"$ref":"#/parameters/ResourceGroupNameParameter"},{"$ref":"#/parameters/RuleNameParameter"},{"$ref":"#/parameters/ApiVersionParameter"},{"name":"parameters","in":"body","required":true,"schema":{"$ref":"#/definitions/LogSearchRuleResourcePatch"},"description":"The parameters of the rule to update."}],"responses":{"default":{"description":"Error response describing why the operation failed.","schema":{"$ref":"#/definitions/ErrorResponse"}},"200":{"description":"Successful request to update an Log Search rule","schema":{"$ref":"#/definitions/LogSearchRuleResource"}}},"x-ms-examples":{"Patch Log Search Rule":{"$ref":"./examples/patchScheduledQueryRules.json"}}},"delete":{"description":"Deletes a Log Search rule","tags":["scheduledQueryRules"],"operationId":"ScheduledQueryRules_Delete","parameters":[{"$ref":"#/parameters/ResourceGroupNameParameter"},{"$ref":"#/parameters/RuleNameParameter"},{"$ref":"#/parameters/ApiVersionParameter"},{"$ref":"#/parameters/SubscriptionIdParameter"}],"responses":{"default":{"description":"Error response describing why the operation failed.","schema":{"$ref":"#/definitions/ErrorResponse"}},"200":{"description":"Successful request to delete a Log Search rule"},"204":{"description":"No Content. Resource not found"}},"x-ms-examples":{"Delete rule":{"$ref":"./examples/deleteScheduledQueryRules.json"}}}},"/subscriptions/{subscriptionId}/providers/microsoft.insights/scheduledQueryRules":{"get":{"tags":["scheduledQueryRules"],"operationId":"ScheduledQueryRules_ListBySubscription","description":"List the Log Search rules within a subscription group.","parameters":[{"$ref":"#/parameters/ApiVersionParameter"},{"$ref":"#/parameters/FilterParameter"},{"$ref":"#/parameters/SubscriptionIdParameter"}],"responses":{"default":{"description":"Error response describing why the operation failed.","schema":{"$ref":"#/definitions/ErrorResponse"}},"200":{"description":"Successful request for a list of alert rules","schema":{"$ref":"#/definitions/LogSearchRuleResourceCollection"}}},"x-ms-pageable":{"nextLinkName":null},"x-ms-examples":{"List rules":{"$ref":"./examples/listScheduledQueryRules.json"}},"x-ms-odata":"#/definitions/LogSearchRuleResource"}},"/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/microsoft.insights/scheduledQueryRules":{"get":{"tags":["scheduledQueryRules"],"operationId":"ScheduledQueryRules_ListByResourceGroup","description":"List the Log Search rules within a resource group.","parameters":[{"$ref":"#/parameters/ResourceGroupNameParameter"},{"$ref":"#/parameters/ApiVersionParameter"},{"$ref":"#/parameters/FilterParameter"},{"$ref":"#/parameters/SubscriptionIdParameter"}],"responses":{"default":{"description":"Error response describing why the operation failed.","schema":{"$ref":"#/definitions/ErrorResponse"}},"200":{"description":"Successful request for a list of alert rules","schema":{"$ref":"#/definitions/LogSearchRuleResourceCollection"}}},"x-ms-pageable":{"nextLinkName":null},"x-ms-examples":{"List rules":{"$ref":"./examples/listScheduledQueryRules.json"}},"x-ms-odata":"#/definitions/LogSearchRuleResource"}}},"definitions":{"Resource":{"properties":{"id":{"type":"string","readOnly":true,"description":"Azure resource Id"},"name":{"type":"string","readOnly":true,"description":"Azure resource name"},"type":{"type":"string","readOnly":true,"description":"Azure resource type"},"location":{"type":"string","description":"Resource location","x-ms-mutability":["create","read"]},"tags":{"additionalProperties":{"type":"string"},"description":"Resource tags"}},"required":["location"],"x-ms-azure-resource":true,"description":"An azure resource object"},"LogSearchRuleResource":{"type":"object","allOf":[{"$ref":"#/definitions/Resource"}],"required":["properties"],"properties":{"properties":{"x-ms-client-flatten":true,"$ref":"#/definitions/LogSearchRule","description":"The rule properties of the resource."}},"description":"The Log Search Rule resource."},"LogSearchRuleResourcePatch":{"properties":{"tags":{"additionalProperties":{"type":"string"},"description":"Resource tags"},"properties":{"x-ms-client-flatten":true,"$ref":"#/definitions/LogSearchRulePatch","description":"The log search rule properties of the resource."}},"description":"The log search rule resource for patch operations."},"LogSearchRuleResourceCollection":{"properties":{"value":{"type":"array","items":{"$ref":"#/definitions/LogSearchRuleResource"},"description":"The values for the Log Search Rule resources."}},"description":"Represents a collection of Log Search rule resources."},"Source":{"type":"object","description":"Specifies the log search query.","properties":{"query":{"type":"string","description":"Log search query. Required for action type - AlertingAction"},"authorizedResources":{"type":"array","items":{"type":"string"},"description":"List of Resource referred into query"},"dataSourceId":{"type":"string","description":"The resource uri over which log search query is to be run."},"queryType":{"$ref":"#/definitions/QueryType","description":"Set value to 'ResultCount' ."}},"required":["dataSourceId"]},"Schedule":{"type":"object","description":"Defines how often to run the search and the time interval.","properties":{"frequencyInMinutes":{"type":"integer","format":"int32","description":"frequency (in minutes) at which rule condition should be evaluated."},"timeWindowInMinutes":{"type":"integer","format":"int32","description":"Time window for which data needs to be fetched for query (should be greater than or equal to frequencyInMinutes)."}},"required":["frequencyInMinutes","timeWindowInMinutes"]},"TriggerCondition":{"description":"The condition that results in the Log Search rule.","properties":{"thresholdOperator":{"$ref":"#/definitions/ConditionalOperator","description":"Evaluation operation for rule - 'GreaterThan' or 'LessThan."},"threshold":{"description":"Result or count threshold based on which rule should be triggered.","format":"double","type":"number"},"metricTrigger":{"$ref":"#/definitions/LogMetricTrigger","description":"Trigger condition for metric query rule"}},"required":["thresholdOperator","threshold"],"type":"object"},"AzNsActionGroup":{"type":"object","properties":{"actionGroup":{"type":"array","description":"Azure Action Group reference.","items":{"type":"string"}},"emailSubject":{"type":"string","description":"Custom subject override for all email ids in Azure action group"},"customWebhookPayload":{"type":"string","description":"Custom payload to be sent for all webook URI in Azure action group"}},"description":"Azure action group"},"LogMetricTrigger":{"type":"object","properties":{"thresholdOperator":{"$ref":"#/definitions/ConditionalOperator","description":"Evaluation operation for Metric -'GreaterThan' or 'LessThan' or 'Equal'."},"threshold":{"format":"double","type":"number","description":"The threshold of the metric trigger."},"metricTriggerType":{"$ref":"#/definitions/MetricTriggerType","description":"Metric Trigger Type - 'Consecutive' or 'Total'"},"metricColumn":{"type":"string","description":"Evaluation of metric on a particular column"}},"description":"A log metrics trigger descriptor."},"ConditionalOperator":{"type":"string","enum":["GreaterThan","LessThan","Equal"],"x-ms-enum":{"name":"ConditionalOperator","modelAsString":true},"description":"Result Condition Evaluation criteria. Supported Values - 'GreaterThan' or 'LessThan' or 'Equal'."},"MetricTriggerType":{"type":"string","enum":["Consecutive","Total"],"x-ms-enum":{"name":"metricTriggerType","modelAsString":true},"description":"Metric Trigger Evaluation Type"},"AlertSeverity":{"type":"string","enum":["0","1","2","3","4"],"x-ms-enum":{"name":"AlertSeverity","modelAsString":true},"description":"Severity Level of Alert"},"QueryType":{"type":"string","enum":["ResultCount"],"x-ms-enum":{"name":"QueryType","modelAsString":true},"description":"Set value to 'ResultAcount'"},"LogSearchRule":{"description":"Log Search Rule Definition","properties":{"description":{"type":"string","description":"The description of the Log Search rule."},"enabled":{"type":"string","description":"The flag which indicates whether the Log Search rule is enabled. Value should be true or false","enum":["true","false"],"x-ms-enum":{"name":"enabled","modelAsString":true}},"lastUpdatedTime":{"readOnly":true,"type":"string","format":"date-time","description":"Last time the rule was updated in IS08601 format."},"provisioningState":{"readOnly":true,"type":"string","enum":["Succeeded","Deploying","Canceled","Failed"],"x-ms-enum":{"name":"provisioningState","modelAsString":true},"description":"Provisioning state of the scheduledquery rule"},"source":{"$ref":"#/definitions/Source","description":"Data Source against which rule will Query Data"},"schedule":{"$ref":"#/definitions/Schedule","description":"Schedule (Frequnecy, Time Window) for rule. Required for action type - AlertingAction"},"action":{"$ref":"#/definitions/Action","description":"Action needs to be taken on rule execution."}},"required":["source","action"]},"LogSearchRulePatch":{"description":"Log Search Rule Definition for Patching","properties":{"enabled":{"type":"string","description":"The flag which indicates whether the Log Search rule is enabled. Value should be true or false","enum":["true","false"],"x-ms-enum":{"name":"enabled","modelAsString":true}}}},"Action":{"type":"object","discriminator":"odata.type","properties":{"odata.type":{"type":"string","description":"Specifies the action. Supported values - AlertingAction, LogToMetricAction"}},"required":["odata.type"],"description":"Action descriptor."},"AlertingAction":{"description":"Specifiy action need to be taken when rule type is Alert","x-ms-discriminator-value":"Microsoft.WindowsAzure.Management.Monitoring.Alerts.Models.Microsoft.AppInsights.Nexus.DataContracts.Resources.ScheduledQueryRules.AlertingAction","type":"object","allOf":[{"$ref":"#/definitions/Action"}],"properties":{"severity":{"$ref":"#/definitions/AlertSeverity","description":"Severity of the alert"},"aznsAction":{"$ref":"#/definitions/AzNsActionGroup","description":"Azure action group reference."},"throttlingInMin":{"type":"integer","format":"int32","description":"time (in minutes) for which Alerts should be throttled or suppressed."},"trigger":{"$ref":"#/definitions/TriggerCondition","description":"The trigger condition that results in the alert rule being."}},"required":["aznsAction","trigger","severity"]},"Dimension":{"type":"object","description":"Specifies the criteria for converting log to metric.","properties":{"name":{"type":"string","description":"Name of the dimension"},"operator":{"type":"string","description":"Operator for dimension values","enum":["Include"],"x-ms-enum":{"name":"operator","modelAsString":true}},"values":{"type":"array","items":{"type":"string"},"description":"List of dimension values"}},"required":["name","operator","values"]},"Criteria":{"type":"object","description":"Specifies the criteria for converting log to metric.","properties":{"metricName":{"type":"string","description":"Name of the metric"},"dimensions":{"type":"array","items":{"$ref":"#/definitions/Dimension"},"description":"List of Dimensions for creating metric"}},"required":["metricName"]},"LogToMetricAction":{"description":"Specifiy action need to be taken when rule type is converting log to metric","x-ms-discriminator-value":"Microsoft.WindowsAzure.Management.Monitoring.Alerts.Models.Microsoft.AppInsights.Nexus.DataContracts.Resources.ScheduledQueryRules.LogToMetricAction","type":"object","allOf":[{"$ref":"#/definitions/Action"}],"properties":{"criteria":{"$ref":"#/definitions/Criteria","description":"Severity of the alert"}},"required":["criteria"]},"ErrorResponse":{"description":"Describes the format of Error response.","type":"object","properties":{"code":{"description":"Error code","type":"string"},"message":{"description":"Error message indicating why the operation failed.","type":"string"}}}},"parameters":{"SubscriptionIdParameter":{"name":"subscriptionId","in":"path","required":true,"type":"string","description":"The Azure subscription Id."},"ApiVersionParameter":{"name":"api-version","in":"query","required":true,"type":"string","description":"Client Api Version."},"ResourceGroupNameParameter":{"name":"resourceGroupName","in":"path","required":true,"type":"string","description":"The name of the resource group.","x-ms-parameter-location":"method"},"RuleNameParameter":{"name":"ruleName","in":"path","required":true,"type":"string","description":"The name of the rule.","x-ms-parameter-location":"method"},"FilterParameter":{"name":"$filter","in":"query","required":false,"type":"string","description":"The filter to apply on the operation. For more information please see https://msdn.microsoft.com/en-us/library/azure/dn931934.aspx","x-ms-parameter-location":"method"}}} \ No newline at end of file diff --git a/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/swashbuckle.json b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/swashbuckle.json new file mode 100644 index 0000000000..57b917e213 --- /dev/null +++ b/src/Tools/Extensions.ApiDescription.Client/test/TestProjects/files/swashbuckle.json @@ -0,0 +1,155 @@ +{ + "swagger": "2.0", + "info": { + "version": "and so is version", + "title": "title is required" + }, + "paths": { + "/api/Values/matches": { + "post": { + "tags": [ + "Values" + ], + "operationId": "ApiValuesMatchesPost", + "consumes": [ + "application/json-patch+json", + "application/json", + "text/json", + "application/*+json", + "application/xml", + "text/xml", + "application/*+xml" + ], + "produces": [ + "text/plain", + "application/json", + "text/json", + "application/xml", + "text/xml" + ], + "parameters": [ + { + "name": "possibilities", + "in": "body", + "required": false, + "schema": { + "uniqueItems": false, + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "comparisonType", + "in": "query", + "required": false, + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "uniqueItems": false, + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "/api/Values": { + "get": { + "tags": [ + "Values" + ], + "operationId": "ApiValuesGet", + "consumes": [], + "produces": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "Success", + "schema": { + "uniqueItems": false, + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "post": { + "tags": [ + "Values" + ], + "operationId": "ApiValuesPost", + "consumes": [ + "application/xml" + ], + "produces": [], + "parameters": [ + { + "name": "value", + "in": "body", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/api/Values/too": { + "get": { + "tags": [ + "Values" + ], + "operationId": "ApiValuesTooGet", + "consumes": [], + "produces": [ + "application/xml" + ], + "parameters": [], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/Model" + } + } + } + } + } + }, + "definitions": { + "Model": { + "type": "object", + "properties": { + "userName": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/src/Tools/Tools.sln b/src/Tools/Tools.sln index 1de511f5cc..f25c3fc982 100644 --- a/src/Tools/Tools.sln +++ b/src/Tools/Tools.sln @@ -23,22 +23,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.dotnet-openapi", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-microsoft.openapi.Tests", "Microsoft.dotnet-openapi\test\dotnet-microsoft.openapi.Tests.csproj", "{26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions.ApiDescription.Client", "Extensions.ApiDescription.Client", "{78610083-1FCE-47F5-AB4D-AF0E1313B351}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A0D46647-EF66-456E-9F79-134985E7445E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Client", "Extensions.ApiDescription.Client\src\Microsoft.Extensions.ApiDescription.Client.csproj", "{B29B2627-3604-4FDB-A976-EF1E077F5316}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions.ApiDescription.Server", "Extensions.ApiDescription.Server", "{003EA860-5DFC-40AE-87C0-9D21BB2C68D7}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4110117E-3C28-4064-A7A3-B112BD6F8CB9}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-getdocument", "dotnet-getdocument\src\dotnet-getdocument.csproj", "{160A445F-7E1F-430D-9403-41F7F6F4A16E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Server", "Extensions.ApiDescription.Server\src\Microsoft.Extensions.ApiDescription.Server.csproj", "{233119FC-E4C1-421C-89AE-1A445C5A947F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetDocumentInsider", "GetDocumentInsider\src\GetDocumentInsider.csproj", "{EB63AECB-B898-475D-90F7-FE61F9C1CCC6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Client.Tests", "Extensions.ApiDescription.Client\test\Microsoft.Extensions.ApiDescription.Client.Tests.csproj", "{2C62584B-EC31-40C8-819B-E46334645AE5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,38 +49,6 @@ Global {63F7E822-D1E2-4C41-8ABF-60B9E3A9C54C}.Debug|Any CPU.Build.0 = Debug|Any CPU {63F7E822-D1E2-4C41-8ABF-60B9E3A9C54C}.Release|Any CPU.ActiveCfg = Release|Any CPU {63F7E822-D1E2-4C41-8ABF-60B9E3A9C54C}.Release|Any CPU.Build.0 = Release|Any CPU - {0D6D5693-7E0C-4FE8-B4AA-21207B2650AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D6D5693-7E0C-4FE8-B4AA-21207B2650AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D6D5693-7E0C-4FE8-B4AA-21207B2650AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D6D5693-7E0C-4FE8-B4AA-21207B2650AA}.Release|Any CPU.Build.0 = Release|Any CPU - {7BBDBDA2-299F-4C36-8338-23C525901DE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7BBDBDA2-299F-4C36-8338-23C525901DE0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7BBDBDA2-299F-4C36-8338-23C525901DE0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7BBDBDA2-299F-4C36-8338-23C525901DE0}.Release|Any CPU.Build.0 = Release|Any CPU - {1EC6FA27-40A5-433F-8CA1-636E7ED8863E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1EC6FA27-40A5-433F-8CA1-636E7ED8863E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1EC6FA27-40A5-433F-8CA1-636E7ED8863E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1EC6FA27-40A5-433F-8CA1-636E7ED8863E}.Release|Any CPU.Build.0 = Release|Any CPU - {15FB0E39-1A28-4325-AD3C-76352516C80D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {15FB0E39-1A28-4325-AD3C-76352516C80D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {15FB0E39-1A28-4325-AD3C-76352516C80D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15FB0E39-1A28-4325-AD3C-76352516C80D}.Release|Any CPU.Build.0 = Release|Any CPU - {B29B2627-3604-4FDB-A976-EF1E077F5316}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B29B2627-3604-4FDB-A976-EF1E077F5316}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B29B2627-3604-4FDB-A976-EF1E077F5316}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B29B2627-3604-4FDB-A976-EF1E077F5316}.Release|Any CPU.Build.0 = Release|Any CPU - {160A445F-7E1F-430D-9403-41F7F6F4A16E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {160A445F-7E1F-430D-9403-41F7F6F4A16E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {160A445F-7E1F-430D-9403-41F7F6F4A16E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {160A445F-7E1F-430D-9403-41F7F6F4A16E}.Release|Any CPU.Build.0 = Release|Any CPU - {233119FC-E4C1-421C-89AE-1A445C5A947F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {233119FC-E4C1-421C-89AE-1A445C5A947F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {233119FC-E4C1-421C-89AE-1A445C5A947F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {233119FC-E4C1-421C-89AE-1A445C5A947F}.Release|Any CPU.Build.0 = Release|Any CPU - {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Release|Any CPU.Build.0 = Release|Any CPU {98550159-E04E-44EB-A969-E5BF12444B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {98550159-E04E-44EB-A969-E5BF12444B94}.Debug|Any CPU.Build.0 = Debug|Any CPU {98550159-E04E-44EB-A969-E5BF12444B94}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -109,17 +73,31 @@ Global {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Debug|Any CPU.Build.0 = Debug|Any CPU {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Release|Any CPU.ActiveCfg = Release|Any CPU {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F}.Release|Any CPU.Build.0 = Release|Any CPU + {B29B2627-3604-4FDB-A976-EF1E077F5316}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B29B2627-3604-4FDB-A976-EF1E077F5316}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B29B2627-3604-4FDB-A976-EF1E077F5316}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B29B2627-3604-4FDB-A976-EF1E077F5316}.Release|Any CPU.Build.0 = Release|Any CPU + {160A445F-7E1F-430D-9403-41F7F6F4A16E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {160A445F-7E1F-430D-9403-41F7F6F4A16E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {160A445F-7E1F-430D-9403-41F7F6F4A16E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {160A445F-7E1F-430D-9403-41F7F6F4A16E}.Release|Any CPU.Build.0 = Release|Any CPU + {233119FC-E4C1-421C-89AE-1A445C5A947F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {233119FC-E4C1-421C-89AE-1A445C5A947F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {233119FC-E4C1-421C-89AE-1A445C5A947F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {233119FC-E4C1-421C-89AE-1A445C5A947F}.Release|Any CPU.Build.0 = Release|Any CPU + {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB63AECB-B898-475D-90F7-FE61F9C1CCC6}.Release|Any CPU.Build.0 = Release|Any CPU + {2C62584B-EC31-40C8-819B-E46334645AE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C62584B-EC31-40C8-819B-E46334645AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C62584B-EC31-40C8-819B-E46334645AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C62584B-EC31-40C8-819B-E46334645AE5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {A0D46647-EF66-456E-9F79-134985E7445E} = {78610083-1FCE-47F5-AB4D-AF0E1313B351} - {B29B2627-3604-4FDB-A976-EF1E077F5316} = {A0D46647-EF66-456E-9F79-134985E7445E} - {4110117E-3C28-4064-A7A3-B112BD6F8CB9} = {003EA860-5DFC-40AE-87C0-9D21BB2C68D7} - {160A445F-7E1F-430D-9403-41F7F6F4A16E} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9} - {233119FC-E4C1-421C-89AE-1A445C5A947F} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9} - {EB63AECB-B898-475D-90F7-FE61F9C1CCC6} = {4110117E-3C28-4064-A7A3-B112BD6F8CB9} {E16F10C8-5FC3-420B-AB33-D6E5CBE89B75} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} {63F7E822-D1E2-4C41-8ABF-60B9E3A9C54C} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44} {98550159-E04E-44EB-A969-E5BF12444B94} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} @@ -128,6 +106,12 @@ Global {25F8DCC4-4571-42F7-BA0F-5C2D5A802297} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44} {C806041C-30F2-4B27-918A-5FF3576B833B} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} {26BBA8A7-0F69-4C5F-B1C2-16B3320FFE3F} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44} + {B29B2627-3604-4FDB-A976-EF1E077F5316} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} + {003EA860-5DFC-40AE-87C0-9D21BB2C68D7} = {E01EE27B-6CF9-4707-9849-5BA2ABA825F2} + {160A445F-7E1F-430D-9403-41F7F6F4A16E} = {003EA860-5DFC-40AE-87C0-9D21BB2C68D7} + {233119FC-E4C1-421C-89AE-1A445C5A947F} = {003EA860-5DFC-40AE-87C0-9D21BB2C68D7} + {EB63AECB-B898-475D-90F7-FE61F9C1CCC6} = {003EA860-5DFC-40AE-87C0-9D21BB2C68D7} + {2C62584B-EC31-40C8-819B-E46334645AE5} = {2C485EAF-E4DE-4D14-8AE1-330641E17D44} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EC668D8E-97B9-4758-9E5C-2E5DD6B9137B}