diff --git a/Mvc.NoFun.sln b/Mvc.NoFun.sln index 3995a5463a..0167992a7f 100644 --- a/Mvc.NoFun.sln +++ b/Mvc.NoFun.sln @@ -117,6 +117,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Ap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mvc.Api.Analyzers.Test", "test\Mvc.Api.Analyzers.Test\Mvc.Api.Analyzers.Test.csproj", "{71C626FC-6408-494B-A127-38CB64F71324}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-getdocument", "src\dotnet-getdocument\dotnet-getdocument.csproj", "{4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetDocumentInsider", "src\GetDocumentInsider\GetDocumentInsider.csproj", "{2F683CF8-B055-46AE-BF83-9D1307F8D45F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Client", "src\Microsoft.Extensions.ApiDescription.Client\Microsoft.Extensions.ApiDescription.Client.csproj", "{34E3C302-B767-40C8-B538-3EE2BD4000C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -575,6 +581,42 @@ Global {71C626FC-6408-494B-A127-38CB64F71324}.Release|Mixed Platforms.Build.0 = Release|Any CPU {71C626FC-6408-494B-A127-38CB64F71324}.Release|x86.ActiveCfg = Release|Any CPU {71C626FC-6408-494B-A127-38CB64F71324}.Release|x86.Build.0 = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|x86.Build.0 = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|Any CPU.Build.0 = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|x86.ActiveCfg = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|x86.Build.0 = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|x86.Build.0 = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|Any CPU.Build.0 = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|x86.ActiveCfg = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|x86.Build.0 = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|x86.Build.0 = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|Any CPU.Build.0 = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|x86.ActiveCfg = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -619,6 +661,9 @@ Global {92D959F2-66B8-490A-BA33-DA4421EBC948} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {1B398182-9EAE-400B-A2BD-EFFAC0168A36} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {71C626FC-6408-494B-A127-38CB64F71324} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {2F683CF8-B055-46AE-BF83-9D1307F8D45F} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {34E3C302-B767-40C8-B538-3EE2BD4000C4} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D003597F-372F-4068-A2F0-353BE3C3B39A} diff --git a/Mvc.sln b/Mvc.sln index 775683a5fa..ce10a95245 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -178,6 +178,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Ap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorRendering", "benchmarkapps\RazorRendering\RazorRendering.csproj", "{D7C6A696-F232-4288-BCCD-367407E4A934}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-getdocument", "src\dotnet-getdocument\dotnet-getdocument.csproj", "{4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GetDocumentInsider", "src\GetDocumentInsider\GetDocumentInsider.csproj", "{2F683CF8-B055-46AE-BF83-9D1307F8D45F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.ApiDescription.Client", "src\Microsoft.Extensions.ApiDescription.Client\Microsoft.Extensions.ApiDescription.Client.csproj", "{34E3C302-B767-40C8-B538-3EE2BD4000C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -938,6 +944,42 @@ Global {D7C6A696-F232-4288-BCCD-367407E4A934}.Release|Mixed Platforms.Build.0 = Release|Any CPU {D7C6A696-F232-4288-BCCD-367407E4A934}.Release|x86.ActiveCfg = Release|Any CPU {D7C6A696-F232-4288-BCCD-367407E4A934}.Release|x86.Build.0 = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Debug|x86.Build.0 = Debug|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|Any CPU.Build.0 = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|x86.ActiveCfg = Release|Any CPU + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6}.Release|x86.Build.0 = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Debug|x86.Build.0 = Debug|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|Any CPU.Build.0 = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|x86.ActiveCfg = Release|Any CPU + {2F683CF8-B055-46AE-BF83-9D1307F8D45F}.Release|x86.Build.0 = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Debug|x86.Build.0 = Debug|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|Any CPU.Build.0 = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|x86.ActiveCfg = Release|Any CPU + {34E3C302-B767-40C8-B538-3EE2BD4000C4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1010,6 +1052,9 @@ Global {DD7B9F20-354C-4D9E-8C8A-8AE6E7595A87} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {3B550487-10E4-4E6D-9CEF-B1B4CA1253DA} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {D7C6A696-F232-4288-BCCD-367407E4A934} = {2859F266-673A-45A2-9E3C-7B39C6DDD38E} + {4EDC489F-3EC5-4AE3-9841-A285F40F5FF6} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {2F683CF8-B055-46AE-BF83-9D1307F8D45F} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} + {34E3C302-B767-40C8-B538-3EE2BD4000C4} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A} diff --git a/build/dependencies.props b/build/dependencies.props index 4e7f5dcd1f..304c16413f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -54,9 +54,11 @@ 2.2.0-preview3-35359 2.2.0-preview3-35359 2.2.0-preview3-35359 + 2.0.0 2.2.0-preview3-35359 2.2.0-preview3-35359 5.2.6 + 15.6.82 2.8.0 2.8.0 2.2.0-preview3-35359 @@ -100,8 +102,10 @@ 4.7.49 2.0.3 1.0.1 + 11.0.2 4.5.0 4.5.0 + 4.3.2 4.5.1 0.10.0 2.3.1 diff --git a/src/GetDocumentInsider/AnsiConsole.cs b/src/GetDocumentInsider/AnsiConsole.cs new file mode 100644 index 0000000000..30397229aa --- /dev/null +++ b/src/GetDocumentInsider/AnsiConsole.cs @@ -0,0 +1,15 @@ +// 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; + +namespace GetDocument +{ + internal class AnsiConsole + { + public static readonly AnsiTextWriter _out = new AnsiTextWriter(Console.Out); + + public static void WriteLine(string text) + => _out.WriteLine(text); + } +} diff --git a/src/GetDocumentInsider/AnsiConstants.cs b/src/GetDocumentInsider/AnsiConstants.cs new file mode 100644 index 0000000000..e529180983 --- /dev/null +++ b/src/GetDocumentInsider/AnsiConstants.cs @@ -0,0 +1,20 @@ +// 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. + +namespace GetDocument +{ + internal static class AnsiConstants + { + public const string Reset = "\x1b[22m\x1b[39m"; + public const string Bold = "\x1b[1m"; + public const string Dark = "\x1b[22m"; + public const string Black = "\x1b[30m"; + public const string Red = "\x1b[31m"; + public const string Green = "\x1b[32m"; + public const string Yellow = "\x1b[33m"; + public const string Blue = "\x1b[34m"; + public const string Magenta = "\x1b[35m"; + public const string Cyan = "\x1b[36m"; + public const string Gray = "\x1b[37m"; + } +} diff --git a/src/GetDocumentInsider/AnsiTextWriter.cs b/src/GetDocumentInsider/AnsiTextWriter.cs new file mode 100644 index 0000000000..c8393d4810 --- /dev/null +++ b/src/GetDocumentInsider/AnsiTextWriter.cs @@ -0,0 +1,131 @@ +// 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.Diagnostics; +using System.IO; +using System.Text.RegularExpressions; + +namespace GetDocument +{ + internal class AnsiTextWriter + { + private readonly TextWriter _writer; + + public AnsiTextWriter(TextWriter writer) => _writer = writer; + + public void WriteLine(string text) + { + Interpret(text); + _writer.Write(Environment.NewLine); + } + + private void Interpret(string value) + { + var matches = Regex.Matches(value, "\x1b\\[([0-9]+)?m"); + + var start = 0; + foreach (Match match in matches) + { + var length = match.Index - start; + if (length != 0) + { + _writer.Write(value.Substring(start, length)); + } + + Apply(match.Groups[1].Value); + + start = match.Index + match.Length; + } + + if (start != value.Length) + { + _writer.Write(value.Substring(start)); + } + } + + private static void Apply(string parameter) + { + switch (parameter) + { + case "1": + ApplyBold(); + break; + + case "22": + ResetBold(); + break; + + case "30": + ApplyColor(ConsoleColor.Black); + break; + + case "31": + ApplyColor(ConsoleColor.DarkRed); + break; + + case "32": + ApplyColor(ConsoleColor.DarkGreen); + break; + + case "33": + ApplyColor(ConsoleColor.DarkYellow); + break; + + case "34": + ApplyColor(ConsoleColor.DarkBlue); + break; + + case "35": + ApplyColor(ConsoleColor.DarkMagenta); + break; + + case "36": + ApplyColor(ConsoleColor.DarkCyan); + break; + + case "37": + ApplyColor(ConsoleColor.Gray); + break; + + case "39": + ResetColor(); + break; + + default: + Debug.Fail("Unsupported parameter: " + parameter); + break; + } + } + + private static void ApplyBold() + => Console.ForegroundColor = (ConsoleColor)((int)Console.ForegroundColor | 8); + + private static void ResetBold() + => Console.ForegroundColor = (ConsoleColor)((int)Console.ForegroundColor & 7); + + private static void ApplyColor(ConsoleColor color) + { + var wasBold = ((int)Console.ForegroundColor & 8) != 0; + + Console.ForegroundColor = color; + + if (wasBold) + { + ApplyBold(); + } + } + + private static void ResetColor() + { + var wasBold = ((int)Console.ForegroundColor & 8) != 0; + + Console.ResetColor(); + + if (wasBold) + { + ApplyBold(); + } + } + } +} diff --git a/src/GetDocumentInsider/CodeAnnotations.cs b/src/GetDocumentInsider/CodeAnnotations.cs new file mode 100644 index 0000000000..7a179f24d3 --- /dev/null +++ b/src/GetDocumentInsider/CodeAnnotations.cs @@ -0,0 +1,23 @@ +// 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; + +namespace JetBrains.Annotations +{ + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.Delegate | + AttributeTargets.Field)] + internal sealed class NotNullAttribute : Attribute + { + } + + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | + AttributeTargets.Property | AttributeTargets.Delegate | + AttributeTargets.Field)] + internal sealed class CanBeNullAttribute : Attribute + { + } +} diff --git a/src/GetDocumentInsider/CommandException.cs b/src/GetDocumentInsider/CommandException.cs new file mode 100644 index 0000000000..5d9778e61f --- /dev/null +++ b/src/GetDocumentInsider/CommandException.cs @@ -0,0 +1,20 @@ +// 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; + +namespace GetDocument +{ + internal class CommandException : Exception + { + public CommandException(string message) + : base(message) + { + } + + public CommandException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/GetDocumentInsider/CommandLineUtils/CommandArgument.cs b/src/GetDocumentInsider/CommandLineUtils/CommandArgument.cs new file mode 100644 index 0000000000..3346ea0ecb --- /dev/null +++ b/src/GetDocumentInsider/CommandLineUtils/CommandArgument.cs @@ -0,0 +1,19 @@ +// 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.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal class CommandArgument + { + public CommandArgument() => Values = new List(); + + public string Name { get; set; } + public string Description { get; set; } + public List Values { get; private set; } + public bool MultipleValues { get; set; } + public string Value => Values.FirstOrDefault(); + } +} diff --git a/src/GetDocumentInsider/CommandLineUtils/CommandLineApplication.cs b/src/GetDocumentInsider/CommandLineUtils/CommandLineApplication.cs new file mode 100644 index 0000000000..facbb68ad0 --- /dev/null +++ b/src/GetDocumentInsider/CommandLineUtils/CommandLineApplication.cs @@ -0,0 +1,604 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal class CommandLineApplication + { + private enum ParseOptionResult + { + Succeeded, + ShowHelp, + ShowVersion, + UnexpectedArgs, + } + + // Indicates whether the parser should throw an exception when it runs into an unexpected argument. + // If this field is set to false, the parser will stop parsing when it sees an unexpected argument, and all + // remaining arguments, including the first unexpected argument, will be stored in RemainingArguments property. + private readonly bool _throwOnUnexpectedArg; + + public CommandLineApplication(bool throwOnUnexpectedArg = true) + { + _throwOnUnexpectedArg = throwOnUnexpectedArg; + Options = new List(); + Arguments = new List(); + Commands = new List(); + RemainingArguments = new List(); + Invoke = () => 0; + } + + public CommandLineApplication Parent { get; set; } + public string Name { get; set; } + public string FullName { get; set; } + public string Syntax { get; set; } + public string Description { get; set; } + public List Options { get; private set; } + public CommandOption OptionHelp { get; private set; } + public CommandOption OptionVersion { get; private set; } + public List Arguments { get; private set; } + public List RemainingArguments { get; private set; } + public bool IsShowingInformation { get; protected set; } // Is showing help or version? + public Func Invoke { get; set; } + public Func LongVersionGetter { get; set; } + public Func ShortVersionGetter { get; set; } + public List Commands { get; private set; } + public bool HandleResponseFiles { get; set; } + public bool AllowArgumentSeparator { get; set; } + public bool HandleRemainingArguments { get; set; } + public string ArgumentSeparatorHelpText { get; set; } + + public CommandLineApplication Command(string name, bool throwOnUnexpectedArg = true) + => Command(name, _ => { }, throwOnUnexpectedArg); + + public CommandLineApplication Command(string name, Action configuration, + bool throwOnUnexpectedArg = true) + { + var command = new CommandLineApplication(throwOnUnexpectedArg) { Name = name, Parent = this }; + Commands.Add(command); + configuration(command); + return command; + } + + public CommandOption Option(string template, string description, CommandOptionType optionType) + => Option(template, description, optionType, _ => { }); + + public CommandOption Option(string template, string description, CommandOptionType optionType, Action configuration) + { + var option = new CommandOption(template, optionType) { Description = description }; + Options.Add(option); + configuration(option); + return option; + } + + public CommandArgument Argument(string name, string description, bool multipleValues = false) + => Argument(name, description, _ => { }, multipleValues); + + public CommandArgument Argument(string name, string description, Action configuration, bool multipleValues = false) + { + var lastArg = Arguments.LastOrDefault(); + if (lastArg != null && lastArg.MultipleValues) + { + var message = string.Format("The last argument '{0}' accepts multiple values. No more argument can be added.", + lastArg.Name); + throw new InvalidOperationException(message); + } + + var argument = new CommandArgument { Name = name, Description = description, MultipleValues = multipleValues }; + Arguments.Add(argument); + configuration(argument); + return argument; + } + + public void OnExecute(Func invoke) => Invoke = invoke; + + public void OnExecute(Func> invoke) => Invoke = () => invoke().Result; + + public int Execute(params string[] args) + { + var command = this; + IEnumerator arguments = null; + + if (HandleResponseFiles) + { + args = ExpandResponseFiles(args).ToArray(); + } + + for (var index = 0; index < args.Length; index++) + { + var arg = args[index]; + + var isLongOption = arg.StartsWith("--"); + if (isLongOption || arg.StartsWith("-")) + { + var result = ParseOption(isLongOption, command, args, ref index, out var option); + if (result == ParseOptionResult.ShowHelp) + { + command.ShowHelp(); + return 0; + } + else if (result == ParseOptionResult.ShowVersion) + { + command.ShowVersion(); + return 0; + } + } + else + { + var subcommand = ParseSubCommand(arg, command); + if (subcommand != null) + { + command = subcommand; + } + else + { + if (arguments == null) + { + arguments = new CommandArgumentEnumerator(command.Arguments.GetEnumerator()); + } + + if (arguments.MoveNext()) + { + arguments.Current.Values.Add(arg); + } + else + { + HandleUnexpectedArg(command, args, index, argTypeName: "command or argument"); + } + } + } + } + + return command.Invoke(); + } + + private ParseOptionResult ParseOption( + bool isLongOption, + CommandLineApplication command, + string[] args, + ref int index, + out CommandOption option) + { + option = null; + var result = ParseOptionResult.Succeeded; + var arg = args[index]; + + var optionPrefixLength = isLongOption ? 2 : 1; + var optionComponents = arg.Substring(optionPrefixLength).Split(new[] { ':', '=' }, 2); + var optionName = optionComponents[0]; + + if (isLongOption) + { + option = command.Options.SingleOrDefault( + opt => string.Equals(opt.LongName, optionName, StringComparison.Ordinal)); + } + else + { + option = command.Options.SingleOrDefault( + opt => string.Equals(opt.ShortName, optionName, StringComparison.Ordinal)); + + if (option == null) + { + option = command.Options.SingleOrDefault( + opt => string.Equals(opt.SymbolName, optionName, StringComparison.Ordinal)); + } + } + + if (option == null) + { + if (isLongOption && string.IsNullOrEmpty(optionName) && + !command._throwOnUnexpectedArg && AllowArgumentSeparator) + { + // a stand-alone "--" is the argument separator, so skip it and + // handle the rest of the args as unexpected args + index++; + } + + HandleUnexpectedArg(command, args, index, argTypeName: "option"); + result = ParseOptionResult.UnexpectedArgs; + } + else if (command.OptionHelp == option) + { + result = ParseOptionResult.ShowHelp; + } + else if (command.OptionVersion == option) + { + result = ParseOptionResult.ShowVersion; + } + else + { + if (optionComponents.Length == 2) + { + if (!option.TryParse(optionComponents[1])) + { + command.ShowHint(); + throw new CommandParsingException(command, + $"Unexpected value '{optionComponents[1]}' for option '{optionName}'"); + } + } + else + { + if (option.OptionType == CommandOptionType.NoValue || + option.OptionType == CommandOptionType.BoolValue) + { + // No value is needed for this option + option.TryParse(null); + } + else + { + index++; + arg = args[index]; + if (!option.TryParse(arg)) + { + command.ShowHint(); + throw new CommandParsingException(command, $"Unexpected value '{arg}' for option '{optionName}'"); + + } + } + } + } + + return result; + } + + private static CommandLineApplication ParseSubCommand(string arg, CommandLineApplication command) + { + foreach (var subcommand in command.Commands) + { + if (string.Equals(subcommand.Name, arg, StringComparison.OrdinalIgnoreCase)) + { + return subcommand; + } + } + + return null; + } + + // Helper method that adds a help option + public CommandOption HelpOption(string template) + { + // Help option is special because we stop parsing once we see it + // So we store it separately for further use + OptionHelp = Option(template, "Show help information", CommandOptionType.NoValue); + + return OptionHelp; + } + + public CommandOption VersionOption(string template, + string shortFormVersion, + string longFormVersion = null) + { + if (longFormVersion == null) + { + return VersionOption(template, () => shortFormVersion); + } + else + { + return VersionOption(template, () => shortFormVersion, () => longFormVersion); + } + } + + // Helper method that adds a version option + public CommandOption VersionOption(string template, + Func shortFormVersionGetter, + Func longFormVersionGetter = null) + { + // Version option is special because we stop parsing once we see it + // So we store it separately for further use + OptionVersion = Option(template, "Show version information", CommandOptionType.NoValue); + ShortVersionGetter = shortFormVersionGetter; + LongVersionGetter = longFormVersionGetter ?? shortFormVersionGetter; + + return OptionVersion; + } + + // Show short hint that reminds users to use help option + public void ShowHint() + { + if (OptionHelp != null) + { + Console.WriteLine(string.Format("Specify --{0} for a list of available options and commands.", OptionHelp.LongName)); + } + } + + // Show full help + public void ShowHelp(string commandName = null) + { + var headerBuilder = new StringBuilder("Usage:"); + var usagePrefixLength = headerBuilder.Length; + for (var cmd = this; cmd != null; cmd = cmd.Parent) + { + cmd.IsShowingInformation = true; + if (cmd != this && cmd.Arguments.Any()) + { + var args = string.Join(" ", cmd.Arguments.Select(arg => arg.Name)); + headerBuilder.Insert(usagePrefixLength, string.Format(" {0} {1}", cmd.Name, args)); + } + else + { + headerBuilder.Insert(usagePrefixLength, string.Format(" {0}", cmd.Name)); + } + } + + CommandLineApplication target; + + if (commandName == null || string.Equals(Name, commandName, StringComparison.OrdinalIgnoreCase)) + { + target = this; + } + else + { + target = Commands.SingleOrDefault(cmd => string.Equals(cmd.Name, commandName, StringComparison.OrdinalIgnoreCase)); + + if (target != null) + { + headerBuilder.AppendFormat(" {0}", commandName); + } + else + { + // The command name is invalid so don't try to show help for something that doesn't exist + target = this; + } + + } + + var optionsBuilder = new StringBuilder(); + var commandsBuilder = new StringBuilder(); + var argumentsBuilder = new StringBuilder(); + var argumentSeparatorBuilder = new StringBuilder(); + + var maxArgLen = 0; + for (var cmd = target; cmd != null; cmd = cmd.Parent) + { + if (cmd.Arguments.Any()) + { + if (cmd == target) + { + headerBuilder.Append(" [arguments]"); + } + + if (argumentsBuilder.Length == 0) + { + argumentsBuilder.AppendLine(); + argumentsBuilder.AppendLine("Arguments:"); + } + + maxArgLen = Math.Max(maxArgLen, MaxArgumentLength(cmd.Arguments)); + } + } + + for (var cmd = target; cmd != null; cmd = cmd.Parent) + { + if (cmd.Arguments.Any()) + { + var outputFormat = " {0}{1}"; + foreach (var arg in cmd.Arguments) + { + argumentsBuilder.AppendFormat( + outputFormat, + arg.Name.PadRight(maxArgLen + 2), + arg.Description); + argumentsBuilder.AppendLine(); + } + } + } + + if (target.Options.Any()) + { + headerBuilder.Append(" [options]"); + + optionsBuilder.AppendLine(); + optionsBuilder.AppendLine("Options:"); + var maxOptLen = MaxOptionTemplateLength(target.Options); + var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxOptLen + 2); + foreach (var opt in target.Options) + { + optionsBuilder.AppendFormat(outputFormat, opt.Template, opt.Description); + optionsBuilder.AppendLine(); + } + } + + if (target.Commands.Any()) + { + headerBuilder.Append(" [command]"); + + commandsBuilder.AppendLine(); + commandsBuilder.AppendLine("Commands:"); + var maxCmdLen = MaxCommandLength(target.Commands); + var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxCmdLen + 2); + foreach (var cmd in target.Commands.OrderBy(c => c.Name)) + { + commandsBuilder.AppendFormat(outputFormat, cmd.Name, cmd.Description); + commandsBuilder.AppendLine(); + } + + if (OptionHelp != null) + { + commandsBuilder.AppendLine(); + commandsBuilder.AppendFormat("Use \"{0} [command] --help\" for more information about a command.", Name); + commandsBuilder.AppendLine(); + } + } + + if (target.AllowArgumentSeparator || target.HandleRemainingArguments) + { + if (target.AllowArgumentSeparator) + { + headerBuilder.Append(" [[--] ...]]"); + } + else + { + headerBuilder.Append(" [args]"); + } + + if (!string.IsNullOrEmpty(target.ArgumentSeparatorHelpText)) + { + argumentSeparatorBuilder.AppendLine(); + argumentSeparatorBuilder.AppendLine("Args:"); + argumentSeparatorBuilder.AppendLine($" {target.ArgumentSeparatorHelpText}"); + argumentSeparatorBuilder.AppendLine(); + } + } + + headerBuilder.AppendLine(); + + var nameAndVersion = new StringBuilder(); + nameAndVersion.AppendLine(GetFullNameAndVersion()); + nameAndVersion.AppendLine(); + + Console.Write("{0}{1}{2}{3}{4}{5}", nameAndVersion, headerBuilder, argumentsBuilder, optionsBuilder, commandsBuilder, argumentSeparatorBuilder); + } + + public void ShowVersion() + { + for (var cmd = this; cmd != null; cmd = cmd.Parent) + { + cmd.IsShowingInformation = true; + } + + Console.WriteLine(FullName); + Console.WriteLine(LongVersionGetter()); + } + + public string GetFullNameAndVersion() + => ShortVersionGetter == null ? FullName : string.Format("{0} {1}", FullName, ShortVersionGetter()); + + public void ShowRootCommandFullNameAndVersion() + { + var rootCmd = this; + while (rootCmd.Parent != null) + { + rootCmd = rootCmd.Parent; + } + + Console.WriteLine(rootCmd.GetFullNameAndVersion()); + Console.WriteLine(); + } + + private static int MaxOptionTemplateLength(IEnumerable options) + { + var maxLen = 0; + foreach (var opt in options) + { + maxLen = opt.Template.Length > maxLen ? opt.Template.Length : maxLen; + } + return maxLen; + } + + private static int MaxCommandLength(IEnumerable commands) + { + var maxLen = 0; + foreach (var cmd in commands) + { + maxLen = cmd.Name.Length > maxLen ? cmd.Name.Length : maxLen; + } + return maxLen; + } + + private static int MaxArgumentLength(IEnumerable arguments) + { + var maxLen = 0; + foreach (var arg in arguments) + { + maxLen = arg.Name.Length > maxLen ? arg.Name.Length : maxLen; + } + return maxLen; + } + + private static void HandleUnexpectedArg(CommandLineApplication command, string[] args, int index, string argTypeName) + { + if (command._throwOnUnexpectedArg) + { + command.ShowHint(); + throw new CommandParsingException(command, $"Unrecognized {argTypeName} '{args[index]}'"); + } + else + { + command.RemainingArguments.Add(args[index]); + } + } + + private IEnumerable ExpandResponseFiles(IEnumerable args) + { + foreach (var arg in args) + { + if (!arg.StartsWith("@", StringComparison.Ordinal)) + { + yield return arg; + } + else + { + var fileName = arg.Substring(1); + + var responseFileArguments = ParseResponseFile(fileName); + + // ParseResponseFile can suppress expanding this response file by + // returning null. In that case, we'll treat the response + // file token as a regular argument. + + if (responseFileArguments == null) + { + yield return arg; + } + else + { + foreach (var responseFileArgument in responseFileArguments) + { + yield return responseFileArgument.Trim(); + } + } + } + } + } + + private IEnumerable ParseResponseFile(string fileName) + { + if (!HandleResponseFiles) + { + return null; + } + + if (!File.Exists(fileName)) + { + throw new InvalidOperationException($"Response file '{fileName}' doesn't exist."); + } + + return File.ReadLines(fileName); + } + + private class CommandArgumentEnumerator : IEnumerator + { + private readonly IEnumerator _enumerator; + + public CommandArgumentEnumerator(IEnumerator enumerator) => _enumerator = enumerator; + + public CommandArgument Current => _enumerator.Current; + + object IEnumerator.Current => Current; + + public void Dispose() => _enumerator.Dispose(); + + public bool MoveNext() + { + if (Current == null || !Current.MultipleValues) + { + return _enumerator.MoveNext(); + } + + // If current argument allows multiple values, we don't move forward and + // all later values will be added to current CommandArgument.Values + return true; + } + + public void Reset() => _enumerator.Reset(); + } + } +} diff --git a/src/GetDocumentInsider/CommandLineUtils/CommandLineApplicationExtensions.cs b/src/GetDocumentInsider/CommandLineUtils/CommandLineApplicationExtensions.cs new file mode 100644 index 0000000000..1c43455ee1 --- /dev/null +++ b/src/GetDocumentInsider/CommandLineUtils/CommandLineApplicationExtensions.cs @@ -0,0 +1,18 @@ +// 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. + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal static class CommandLineApplicationExtensions + { + public static CommandOption Option(this CommandLineApplication command, string template, string description) + => command.Option( + template, + description, + template.IndexOf('<') != -1 + ? template.EndsWith(">...") + ? CommandOptionType.MultipleValue + : CommandOptionType.SingleValue + : CommandOptionType.NoValue); + } +} diff --git a/src/GetDocumentInsider/CommandLineUtils/CommandOption.cs b/src/GetDocumentInsider/CommandLineUtils/CommandOption.cs new file mode 100644 index 0000000000..5ba4b78ae3 --- /dev/null +++ b/src/GetDocumentInsider/CommandLineUtils/CommandOption.cs @@ -0,0 +1,125 @@ +// 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.Linq; + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal class CommandOption + { + public CommandOption(string template, CommandOptionType optionType) + { + Template = template; + OptionType = optionType; + Values = new List(); + + foreach (var part in Template.Split(new[] { ' ', '|' }, StringSplitOptions.RemoveEmptyEntries)) + { + if (part.StartsWith("--")) + { + LongName = part.Substring(2); + } + else if (part.StartsWith("-")) + { + var optName = part.Substring(1); + + // If there is only one char and it is not an English letter, it is a symbol option (e.g. "-?") + if (optName.Length == 1 && !IsEnglishLetter(optName[0])) + { + SymbolName = optName; + } + else + { + ShortName = optName; + } + } + else if (part.StartsWith("<") && part.EndsWith(">")) + { + ValueName = part.Substring(1, part.Length - 2); + } + else if (optionType == CommandOptionType.MultipleValue && part.StartsWith("<") && part.EndsWith(">...")) + { + ValueName = part.Substring(1, part.Length - 5); + } + else + { + throw new ArgumentException($"Invalid template pattern '{template}'", nameof(template)); + } + } + + if (string.IsNullOrEmpty(LongName) && string.IsNullOrEmpty(ShortName) && string.IsNullOrEmpty(SymbolName)) + { + throw new ArgumentException($"Invalid template pattern '{template}'", nameof(template)); + } + } + + public string Template { get; set; } + public string ShortName { get; set; } + public string LongName { get; set; } + public string SymbolName { get; set; } + public string ValueName { get; set; } + public string Description { get; set; } + public List Values { get; private set; } + public bool? BoolValue { get; private set; } + public CommandOptionType OptionType { get; private set; } + + public bool TryParse(string value) + { + switch (OptionType) + { + case CommandOptionType.MultipleValue: + Values.Add(value); + break; + case CommandOptionType.SingleValue: + if (Values.Any()) + { + return false; + } + Values.Add(value); + break; + case CommandOptionType.BoolValue: + if (Values.Any()) + { + return false; + } + + if (value == null) + { + // add null to indicate that the option was present, but had no value + Values.Add(null); + BoolValue = true; + } + else + { + if (!bool.TryParse(value, out var boolValue)) + { + return false; + } + + Values.Add(value); + BoolValue = boolValue; + } + break; + case CommandOptionType.NoValue: + if (value != null) + { + return false; + } + // Add a value to indicate that this option was specified + Values.Add("on"); + break; + default: + break; + } + return true; + } + + public bool HasValue() => Values.Any(); + + public string Value() => HasValue() ? Values[0] : null; + + private static bool IsEnglishLetter(char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } +} diff --git a/src/GetDocumentInsider/CommandLineUtils/CommandOptionType.cs b/src/GetDocumentInsider/CommandLineUtils/CommandOptionType.cs new file mode 100644 index 0000000000..5f7d37f029 --- /dev/null +++ b/src/GetDocumentInsider/CommandLineUtils/CommandOptionType.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal enum CommandOptionType + { + MultipleValue, + SingleValue, + BoolValue, + NoValue + } +} diff --git a/src/GetDocumentInsider/CommandLineUtils/CommandParsingException.cs b/src/GetDocumentInsider/CommandLineUtils/CommandParsingException.cs new file mode 100644 index 0000000000..c735ecbf12 --- /dev/null +++ b/src/GetDocumentInsider/CommandLineUtils/CommandParsingException.cs @@ -0,0 +1,15 @@ +// 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; + +namespace Microsoft.DotNet.Cli.CommandLine +{ + internal class CommandParsingException : Exception + { + public CommandParsingException(CommandLineApplication command, string message) + : base(message) => Command = command; + + public CommandLineApplication Command { get; } + } +} diff --git a/src/GetDocumentInsider/Commands/CommandBase.cs b/src/GetDocumentInsider/Commands/CommandBase.cs new file mode 100644 index 0000000000..4f66e51d5e --- /dev/null +++ b/src/GetDocumentInsider/Commands/CommandBase.cs @@ -0,0 +1,39 @@ +// 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 GetDocument.Properties; +using Microsoft.DotNet.Cli.CommandLine; + +namespace GetDocument.Commands +{ + internal abstract class CommandBase + { + public virtual void Configure(CommandLineApplication command) + { + var verbose = command.Option("-v|--verbose", Resources.VerboseDescription); + var noColor = command.Option("--no-color", Resources.NoColorDescription); + var prefixOutput = command.Option("--prefix-output", Resources.PrefixDescription); + + command.HandleResponseFiles = true; + + command.OnExecute( + () => + { + Reporter.IsVerbose = verbose.HasValue(); + Reporter.NoColor = noColor.HasValue(); + Reporter.PrefixOutput = prefixOutput.HasValue(); + + Validate(); + + return Execute(); + }); + } + + protected virtual void Validate() + { + } + + protected virtual int Execute() + => 0; + } +} diff --git a/src/GetDocumentInsider/Commands/GetDocumentCommand.cs b/src/GetDocumentInsider/Commands/GetDocumentCommand.cs new file mode 100644 index 0000000000..f615e4399b --- /dev/null +++ b/src/GetDocumentInsider/Commands/GetDocumentCommand.cs @@ -0,0 +1,175 @@ +// 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.IO; +using System.Linq; +using System.Reflection; +#if NETCOREAPP2_0 +using System.Runtime.Loader; +#endif +using GetDocument.Properties; +using Microsoft.DotNet.Cli.CommandLine; + +namespace GetDocument.Commands +{ + internal class GetDocumentCommand : ProjectCommandBase + { + internal const string FallbackDocumentName = "v1"; + internal const string FallbackMethod = "Generate"; + internal const string FallbackService = "Microsoft.Extensions.ApiDescription.IDocumentProvider"; + private const string WorkerType = "GetDocument.Commands.GetDocumentCommandWorker"; + + private CommandOption _documentName; + private CommandOption _method; + private CommandOption _output; + private CommandOption _service; + private CommandOption _uri; + + public override void Configure(CommandLineApplication command) + { + base.Configure(command); + + _documentName = command.Option( + "--documentName ", + Resources.DocumentDescription(FallbackDocumentName)); + _method = command.Option("--method ", Resources.MethodDescription(FallbackMethod)); + _output = command.Option("--output ", Resources.OutputDescription); + _service = command.Option("--service ", Resources.ServiceDescription(FallbackService)); + _uri = command.Option("--uri ", Resources.UriDescription); + } + + protected override void Validate() + { + base.Validate(); + + if (!_output.HasValue()) + { + throw new CommandException(Resources.MissingOption(_output.LongName)); + } + + if (_method.HasValue() && !_service.HasValue()) + { + throw new CommandException(Resources.MissingOption(_service.LongName)); + } + + if (_service.HasValue() && !_method.HasValue()) + { + throw new CommandException(Resources.MissingOption(_method.LongName)); + } + } + + protected override int Execute() + { + var thisAssembly = typeof(GetDocumentCommand).Assembly; + + var toolsDirectory = ToolsDirectory.Value(); + var packagedAssemblies = Directory + .EnumerateFiles(toolsDirectory, "*.dll") + .Except(new[] { Path.GetFullPath(thisAssembly.Location) }) + .ToDictionary(path => Path.GetFileNameWithoutExtension(path), path => new AssemblyInfo(path)); + + // Explicitly load all assemblies we need first to preserve target project as much as possible. This + // executable is always run in the target project's context (either through location or .deps.json file). + foreach (var keyValuePair in packagedAssemblies) + { + try + { + keyValuePair.Value.Assembly = Assembly.Load(new AssemblyName(keyValuePair.Key)); + } + catch + { + // Ignore all failures because missing assemblies should be loadable from tools directory. + } + } + +#if NETCOREAPP2_0 + AssemblyLoadContext.Default.Resolving += (loadContext, assemblyName) => + { + var name = assemblyName.Name; + if (!packagedAssemblies.TryGetValue(name, out var info)) + { + return null; + } + + var assemblyPath = info.Path; + if (!File.Exists(assemblyPath)) + { + throw new InvalidOperationException( + $"Referenced assembly '{name}' was not found in '{toolsDirectory}'."); + } + + return loadContext.LoadFromAssemblyPath(assemblyPath); + }; + +#elif NET461 + AppDomain.CurrentDomain.AssemblyResolve += (source, eventArgs) => + { + var assemblyName = new AssemblyName(eventArgs.Name); + var name = assemblyName.Name; + if (!packagedAssemblies.TryGetValue(name, out var info)) + { + return null; + } + + var assembly = info.Assembly; + if (assembly != null) + { + // Loaded already + return assembly; + } + + var assemblyPath = info.Path; + if (!File.Exists(assemblyPath)) + { + throw new InvalidOperationException( + $"Referenced assembly '{name}' was not found in '{toolsDirectory}'."); + } + + return Assembly.LoadFile(assemblyPath); + }; +#else +#error target frameworks need to be updated. +#endif + + // Now safe to reference TestHost type. + try + { + var workerType = thisAssembly.GetType(WorkerType, throwOnError: true); + var methodInfo = workerType.GetMethod("Process", BindingFlags.Public | BindingFlags.Static); + + var assemblyPath = AssemblyPath.Value(); + var context = new GetDocumentCommandContext + { + AssemblyPath = assemblyPath, + AssemblyDirectory = Path.GetDirectoryName(assemblyPath), + AssemblyName = Path.GetFileNameWithoutExtension(assemblyPath), + DocumentName = _documentName.Value(), + Method = _method.Value(), + Output = _output.Value(), + Service = _service.Value(), + Uri = _uri.Value(), + }; + + return (int)methodInfo.Invoke(obj: null, parameters: new[] { context }); + } + catch (Exception ex) + { + Console.Error.WriteLine(ex.ToString()); + return 1; + } + } + + private class AssemblyInfo + { + public AssemblyInfo(string path) + { + Path = path; + } + + public string Path { get; } + + public Assembly Assembly { get; set; } + } + } +} diff --git a/src/GetDocumentInsider/Commands/GetDocumentCommandContext.cs b/src/GetDocumentInsider/Commands/GetDocumentCommandContext.cs new file mode 100644 index 0000000000..996e9e9701 --- /dev/null +++ b/src/GetDocumentInsider/Commands/GetDocumentCommandContext.cs @@ -0,0 +1,24 @@ +using System; + +namespace GetDocument.Commands +{ + [Serializable] + public class GetDocumentCommandContext + { + public string AssemblyDirectory { get; set; } + + public string AssemblyName { get; set; } + + public string AssemblyPath { get; set; } + + public string DocumentName { get; set; } + + public string Method { get; set; } + + public string Output { get; set; } + + public string Service { get; set; } + + public string Uri { get; set; } + } +} diff --git a/src/GetDocumentInsider/Commands/GetDocumentCommandWorker.cs b/src/GetDocumentInsider/Commands/GetDocumentCommandWorker.cs new file mode 100644 index 0000000000..c010165e61 --- /dev/null +++ b/src/GetDocumentInsider/Commands/GetDocumentCommandWorker.cs @@ -0,0 +1,260 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using GenerationTasks; +using GetDocument.Properties; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace GetDocument.Commands +{ + internal class GetDocumentCommandWorker + { + public static int Process(GetDocumentCommandContext context) + { + var assemblyName = new AssemblyName(context.AssemblyName); + var assembly = Assembly.Load(assemblyName); + var entryPointType = assembly.EntryPoint?.DeclaringType; + if (entryPointType == null) + { + Reporter.WriteError(Resources.MissingEntryPoint(context.AssemblyPath)); + return 2; + } + + var services = GetServices(entryPointType, context.AssemblyPath, context.AssemblyName); + if (services == null) + { + return 3; + } + + var success = TryProcess(context, services); + if (!success && string.IsNullOrEmpty(context.Uri)) + { + return 4; + } + + var builder = GetBuilder(entryPointType, context.AssemblyPath, context.AssemblyName); + if (builder == null) + { + return 5; + } + + // Mute the HttpsRedirectionMiddleware warning about HTTPS configuration. + builder.ConfigureLogging(loggingBuilder => loggingBuilder.AddFilter( + "Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware", + LogLevel.Error)); + + using (var server = new TestServer(builder)) + { + ProcessAsync(context, server).Wait(); + } + + return 0; + } + + public static bool TryProcess(GetDocumentCommandContext context, IServiceProvider services) + { + var documentName = string.IsNullOrEmpty(context.DocumentName) ? + GetDocumentCommand.FallbackDocumentName : + context.DocumentName; + var methodName = string.IsNullOrEmpty(context.Method) ? + GetDocumentCommand.FallbackMethod : + context.Method; + var serviceName = string.IsNullOrEmpty(context.Service) ? + GetDocumentCommand.FallbackService : + context.Service; + + Reporter.WriteInformation(Resources.UsingDocument(documentName)); + Reporter.WriteInformation(Resources.UsingMethod(methodName)); + Reporter.WriteInformation(Resources.UsingService(serviceName)); + + try + { + var serviceType = Type.GetType(serviceName, throwOnError: true); + var method = serviceType.GetMethod(methodName, new[] { typeof(TextWriter), typeof(string) }); + var service = services.GetRequiredService(serviceType); + + var success = true; + using (var writer = File.CreateText(context.Output)) + { + if (method.ReturnType == typeof(bool)) + { + success = (bool)method.Invoke(service, new object[] { writer, documentName }); + } + else + { + method.Invoke(service, new object[] { writer, documentName }); + } + } + + if (!success) + { + var message = Resources.MethodInvocationFailed(methodName, serviceName, documentName); + if (string.IsNullOrEmpty(context.Uri) && !File.Exists(context.Output)) + { + Reporter.WriteError(message); + } + else + { + Reporter.WriteWarning(message); + } + } + + return success; + } + catch (Exception ex) + { + var message = FormatException(ex); + if (string.IsNullOrEmpty(context.Uri) && !File.Exists(context.Output)) + { + Reporter.WriteError(message); + } + else + { + Reporter.WriteWarning(message); + } + + return false; + } + } + + public static async Task ProcessAsync(GetDocumentCommandContext context, TestServer server) + { + + Debug.Assert(!string.IsNullOrEmpty(context.Uri)); + Reporter.WriteInformation(Resources.UsingUri(context.Uri)); + + var httpClient = server.CreateClient(); + await DownloadFileCore.DownloadAsync( + context.Uri, + context.Output, + httpClient, + new LogWrapper(), + CancellationToken.None, + timeoutSeconds: 60); + } + + // TODO: Use Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Sources once we have dev feed available. + private static IServiceProvider GetServices(Type entryPointType, string assemblyPath, string assemblyName) + { + var args = new[] { Array.Empty() }; + var methodInfo = entryPointType.GetMethod("BuildWebHost"); + if (methodInfo != null) + { + // BuildWebHost (old style has highest priority) + var parameters = methodInfo.GetParameters(); + if (!methodInfo.IsStatic || + parameters.Length != 1 || + typeof(string[]) != parameters[0].ParameterType || + typeof(IWebHost) != methodInfo.ReturnType) + { + Reporter.WriteError( + "BuildWebHost method found in {assemblyPath} does not have expected signature."); + + return null; + } + + try + { + var webHost = (IWebHost)methodInfo.Invoke(obj: null, parameters: args); + + return webHost.Services; + } + catch (Exception ex) + { + Reporter.WriteError($"BuildWebHost method threw: {FormatException(ex)}"); + + return null; + } + } + + if ((methodInfo = entryPointType.GetMethod("CreateWebHostBuilder")) != null) + { + // CreateWebHostBuilder + var parameters = methodInfo.GetParameters(); + if (!methodInfo.IsStatic || + parameters.Length != 1 || + typeof(string[]) != parameters[0].ParameterType || + typeof(IWebHostBuilder) != methodInfo.ReturnType) + { + Reporter.WriteError( + "CreateWebHostBuilder method found in {assemblyPath} does not have expected signature."); + + return null; + } + + try + { + var builder = (IWebHostBuilder)methodInfo.Invoke(obj: null, parameters: args); + + return builder.Build().Services; + } + catch (Exception ex) + { + Reporter.WriteError($"CreateWebHostBuilder method threw: {FormatException(ex)}"); + + return null; + } + } + + // Startup + return new WebHostBuilder().UseStartup(assemblyName).Build().Services; + } + + // TODO: Use Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Sources once we have dev feed available. + private static IWebHostBuilder GetBuilder(Type entryPointType, string assemblyPath, string assemblyName) + { + var methodInfo = entryPointType.GetMethod("BuildWebHost"); + if (methodInfo != null) + { + // BuildWebHost cannot be used. Fall through, most likely to Startup fallback. + Reporter.WriteWarning( + "BuildWebHost method cannot be used. Falling back to minimal Startup configuration."); + } + + methodInfo = entryPointType.GetMethod("CreateWebHostBuilder"); + if (methodInfo != null) + { + // CreateWebHostBuilder + var parameters = methodInfo.GetParameters(); + if (!methodInfo.IsStatic || + parameters.Length != 1 || + typeof(string[]) != parameters[0].ParameterType || + typeof(IWebHostBuilder) != methodInfo.ReturnType) + { + Reporter.WriteError( + "CreateWebHostBuilder method found in {assemblyPath} does not have expected signature."); + + return null; + } + + try + { + var args = new[] { Array.Empty() }; + var builder = (IWebHostBuilder)methodInfo.Invoke(obj: null, parameters: args); + + return builder; + } + catch (Exception ex) + { + Reporter.WriteError($"CreateWebHostBuilder method threw: {FormatException(ex)}"); + + return null; + } + } + + // Startup + return new WebHostBuilder().UseStartup(assemblyName); + } + + private static string FormatException(Exception exception) + { + return $"{exception.GetType().FullName}: {exception.Message}"; + } + } +} diff --git a/src/GetDocumentInsider/Commands/HelpCommandBase.cs b/src/GetDocumentInsider/Commands/HelpCommandBase.cs new file mode 100644 index 0000000000..7e2c89cb5a --- /dev/null +++ b/src/GetDocumentInsider/Commands/HelpCommandBase.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.DotNet.Cli.CommandLine; + +namespace GetDocument.Commands +{ + internal class HelpCommandBase : CommandBase + { + public override void Configure(CommandLineApplication command) + { + base.Configure(command); + + command.HelpOption("-h|--help"); + } + } +} diff --git a/src/GetDocumentInsider/Commands/ProjectCommandBase.cs b/src/GetDocumentInsider/Commands/ProjectCommandBase.cs new file mode 100644 index 0000000000..a1a762f96c --- /dev/null +++ b/src/GetDocumentInsider/Commands/ProjectCommandBase.cs @@ -0,0 +1,38 @@ +// 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 GetDocument.Properties; +using Microsoft.DotNet.Cli.CommandLine; + +namespace GetDocument.Commands +{ + internal abstract class ProjectCommandBase : HelpCommandBase + { + public CommandOption AssemblyPath { get; private set; } + + public CommandOption ToolsDirectory { get; private set; } + + public override void Configure(CommandLineApplication command) + { + base.Configure(command); + + AssemblyPath = command.Option("-a|--assembly ", Resources.AssemblyDescription); + ToolsDirectory = command.Option("--tools-directory ", Resources.ToolsDirectoryDescription); + } + + protected override void Validate() + { + base.Validate(); + + if (!AssemblyPath.HasValue()) + { + throw new CommandException(Resources.MissingOption(AssemblyPath.LongName)); + } + + if (!ToolsDirectory.HasValue()) + { + throw new CommandException(Resources.MissingOption(ToolsDirectory.LongName)); + } + } + } +} diff --git a/src/GetDocumentInsider/GetDocumentInsider.csproj b/src/GetDocumentInsider/GetDocumentInsider.csproj new file mode 100644 index 0000000000..8fe28cd313 --- /dev/null +++ b/src/GetDocumentInsider/GetDocumentInsider.csproj @@ -0,0 +1,39 @@ + + + GetDocument.Insider + GetDocument Command-line Tool inside man + false + Exe + GetDocument + netcoreapp2.0;net461 + + + + + + + + + + + + + + + TextTemplatingFileGenerator + Resources.Designer.cs + + + + + + + + + + True + True + Resources.Designer.tt + + + diff --git a/src/GetDocumentInsider/Json.cs b/src/GetDocumentInsider/Json.cs new file mode 100644 index 0000000000..acb62e449f --- /dev/null +++ b/src/GetDocumentInsider/Json.cs @@ -0,0 +1,19 @@ +// 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 GetDocument.Properties; +using Microsoft.DotNet.Cli.CommandLine; + +namespace GetDocument +{ + internal static class Json + { + public static CommandOption ConfigureOption(CommandLineApplication command) + => command.Option("--json", Resources.JsonDescription); + + public static string Literal(string text) + => text != null + ? "\"" + text.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"" + : "null"; + } +} diff --git a/src/GetDocumentInsider/LogWrapper.cs b/src/GetDocumentInsider/LogWrapper.cs new file mode 100644 index 0000000000..0ab10cb744 --- /dev/null +++ b/src/GetDocumentInsider/LogWrapper.cs @@ -0,0 +1,29 @@ +using System; +using GenerationTasks; + +namespace GetDocument +{ + public class LogWrapper : ILogWrapper + { + public void LogError(string message, params object[] messageArgs) + { + Reporter.WriteError(string.Format(message, messageArgs)); + } + + public void LogError(Exception exception, bool showStackTrace) + { + var message = showStackTrace ? exception.ToString() : exception.Message; + Reporter.WriteError(message); + } + + public void LogInformational(string message, params object[] messageArgs) + { + Reporter.WriteInformation(string.Format(message, messageArgs)); + } + + public void LogWarning(string message, params object[] messageArgs) + { + Reporter.WriteWarning(string.Format(message, messageArgs)); + } + } +} diff --git a/src/GetDocumentInsider/ProductInfo.cs b/src/GetDocumentInsider/ProductInfo.cs new file mode 100644 index 0000000000..22045ee0df --- /dev/null +++ b/src/GetDocumentInsider/ProductInfo.cs @@ -0,0 +1,24 @@ +// 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.Reflection; + +namespace GetDocument +{ + /// + /// This API supports the GetDocument infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static class ProductInfo + { + /// + /// This API supports the GetDocument infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string GetVersion() + => typeof(ProductInfo) + .Assembly + .GetCustomAttribute() + .InformationalVersion; + } +} diff --git a/src/GetDocumentInsider/Program.cs b/src/GetDocumentInsider/Program.cs new file mode 100644 index 0000000000..935fca4de0 --- /dev/null +++ b/src/GetDocumentInsider/Program.cs @@ -0,0 +1,51 @@ +// 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.Text; +using GetDocument.Commands; +using Microsoft.DotNet.Cli.CommandLine; + +namespace GetDocument +{ + internal static class Program + { + private static int Main(string[] args) + { + if (Console.IsOutputRedirected) + { + Console.OutputEncoding = Encoding.UTF8; + } + + var app = new CommandLineApplication(throwOnUnexpectedArg: false) + { + Name = "GetDocument.Insider" + }; + + new GetDocumentCommand().Configure(app); + + try + { + return app.Execute(args); + } + catch (Exception ex) + { + if (ex is CommandException + || ex is CommandParsingException + || (ex is WrappedException wrappedException + && wrappedException.Type == "GetDocument.Design.OperationException")) + { + Reporter.WriteVerbose(ex.ToString()); + } + else + { + Reporter.WriteInformation(ex.ToString()); + } + + Reporter.WriteError(ex.Message); + + return 1; + } + } + } +} diff --git a/src/GetDocumentInsider/Properties/Resources.Designer.cs b/src/GetDocumentInsider/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..90463782ab --- /dev/null +++ b/src/GetDocumentInsider/Properties/Resources.Designer.cs @@ -0,0 +1,207 @@ +// + +using System; +using System.Reflection; +using System.Resources; +using JetBrains.Annotations; + +namespace GetDocument.Properties +{ + /// + /// This API supports the GetDocument infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("GetDocument.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The assembly to use. + /// + public static string AssemblyDescription + => GetString("AssemblyDescription"); + + /// + /// Show JSON output. + /// + public static string JsonDescription + => GetString("JsonDescription"); + + /// + /// Missing required option '--{option}'. + /// + public static string MissingOption([CanBeNull] object option) + => string.Format( + GetString("MissingOption", nameof(option)), + option); + + /// + /// Do not colorize output. + /// + public static string NoColorDescription + => GetString("NoColorDescription"); + + /// + /// The file to write the result to. + /// + public static string OutputDescription + => GetString("OutputDescription"); + + /// + /// Prefix console output with logging level. + /// + public static string PrefixDescription + => GetString("PrefixDescription"); + + /// + /// Using application base '{appBase}'. + /// + public static string UsingApplicationBase([CanBeNull] object appBase) + => string.Format( + GetString("UsingApplicationBase", nameof(appBase)), + appBase); + + /// + /// Using assembly '{assembly}'. + /// + public static string UsingAssembly([CanBeNull] object assembly) + => string.Format( + GetString("UsingAssembly", nameof(assembly)), + assembly); + + /// + /// Using configuration file '{config}'. + /// + public static string UsingConfigurationFile([CanBeNull] object config) + => string.Format( + GetString("UsingConfigurationFile", nameof(config)), + config); + + /// + /// Show verbose output. + /// + public static string VerboseDescription + => GetString("VerboseDescription"); + + /// + /// Writing '{file}'... + /// + public static string WritingFile([CanBeNull] object file) + => string.Format( + GetString("WritingFile", nameof(file)), + file); + + /// + /// Using working directory '{workingDirectory}'. + /// + public static string UsingWorkingDirectory([CanBeNull] object workingDirectory) + => string.Format( + GetString("UsingWorkingDirectory", nameof(workingDirectory)), + workingDirectory); + + /// + /// Location from which inside man was copied (in the .NET Framework case) or loaded. + /// + public static string ToolsDirectoryDescription + => GetString("ToolsDirectoryDescription"); + + /// + /// The URI to download the document from. + /// + public static string UriDescription + => GetString("UriDescription"); + + /// + /// The name of the method to invoke on the '--service' instance. Default value '{defaultMethod}'. + /// + public static string MethodDescription([CanBeNull] object defaultMethod) + => string.Format( + GetString("MethodDescription", nameof(defaultMethod)), + defaultMethod); + + /// + /// The qualified name of the service type to retrieve from dependency injection. Default value '{defaultService}'. + /// + public static string ServiceDescription([CanBeNull] object defaultService) + => string.Format( + GetString("ServiceDescription", nameof(defaultService)), + defaultService); + + /// + /// Missing required option '--{option1}' or '--{option2}'. + /// + public static string MissingOptions([CanBeNull] object option1, [CanBeNull] object option2) + => string.Format( + GetString("MissingOptions", nameof(option1), nameof(option2)), + option1, option2); + + /// + /// The name of the document to pass to the '--method' method. Default value '{defaultDocumentName}'. + /// + public static string DocumentDescription([CanBeNull] object defaultDocumentName) + => string.Format( + GetString("DocumentDescription", nameof(defaultDocumentName)), + defaultDocumentName); + + /// + /// Using document name '{documentName}'. + /// + public static string UsingDocument([CanBeNull] object documentName) + => string.Format( + GetString("UsingDocument", nameof(documentName)), + documentName); + + /// + /// Using method '{method}'. + /// + public static string UsingMethod([CanBeNull] object method) + => string.Format( + GetString("UsingMethod", nameof(method)), + method); + + /// + /// Using service '{service}'. + /// + public static string UsingService([CanBeNull] object service) + => string.Format( + GetString("UsingService", nameof(service)), + service); + + /// + /// Using URI '{uri}'. + /// + public static string UsingUri([CanBeNull] object uri) + => string.Format( + GetString("UsingUri", nameof(uri)), + uri); + + /// + /// Method '{method}' of service '{service}' failed to generate document '{documentName}'. + /// + public static string MethodInvocationFailed([CanBeNull] object method, [CanBeNull] object service, [CanBeNull] object documentName) + => string.Format( + GetString("MethodInvocationFailed", nameof(method), nameof(service), nameof(documentName)), + method, service, documentName); + + /// + /// Assembly '{assemblyPath}' does not contain an entry point. + /// + public static string MissingEntryPoint([CanBeNull] object assemblyPath) + => string.Format( + GetString("MissingEntryPoint", nameof(assemblyPath)), + assemblyPath); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + + return value; + } + } +} + diff --git a/src/GetDocumentInsider/Properties/Resources.Designer.tt b/src/GetDocumentInsider/Properties/Resources.Designer.tt new file mode 100644 index 0000000000..3f636a4db5 --- /dev/null +++ b/src/GetDocumentInsider/Properties/Resources.Designer.tt @@ -0,0 +1,6 @@ +<# + Session["ResourceFile"] = "Resources.resx"; + Session["AccessModifier"] = "internal"; + Session["NoDiagnostics"] = true; +#> +<#@ include file="..\..\tools\Resources.tt" #> diff --git a/src/GetDocumentInsider/Properties/Resources.resx b/src/GetDocumentInsider/Properties/Resources.resx new file mode 100644 index 0000000000..f5c08e3a40 --- /dev/null +++ b/src/GetDocumentInsider/Properties/Resources.resx @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The assembly to use. + + + Show JSON output. + + + Missing required option '--{option}'. + + + Do not colorize output. + + + The file to write the result to. + + + Prefix console output with logging level. + + + Using application base '{appBase}'. + + + Using assembly '{assembly}'. + + + Using configuration file '{config}'. + + + Show verbose output. + + + Writing '{file}'... + + + Using working directory '{workingDirectory}'. + + + Location from which inside man was copied (in the .NET Framework case) or loaded. + + + The URI to download the document from. + + + The name of the method to invoke on the '--service' instance. Default value '{defaultMethod}'. + + + The qualified name of the service type to retrieve from dependency injection. Default value '{defaultService}'. + + + Missing required option '--{option1}' or '--{option2}'. + + + The name of the document to pass to the '--method' method. Default value '{defaultDocumentName}'. + + + Using document name '{documentName}'. + + + Using method '{method}'. + + + Using service '{service}'. + + + Using URI '{uri}'. + + + Method '{method}' of service '{service}' failed to generate document '{documentName}'. + + + Assembly '{assemblyPath}' does not contain an entry point. + + \ No newline at end of file diff --git a/src/GetDocumentInsider/Reporter.cs b/src/GetDocumentInsider/Reporter.cs new file mode 100644 index 0000000000..b7c0c264a5 --- /dev/null +++ b/src/GetDocumentInsider/Reporter.cs @@ -0,0 +1,58 @@ +// 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.Linq; +using static GetDocument.AnsiConstants; + +namespace GetDocument +{ + internal static class Reporter + { + public static bool IsVerbose { get; set; } + public static bool NoColor { get; set; } + public static bool PrefixOutput { get; set; } + + public static string Colorize(string value, Func colorizeFunc) + => NoColor ? value : colorizeFunc(value); + + public static void WriteError(string message) + => WriteLine(Prefix("error: ", Colorize(message, x => Bold + Red + x + Reset))); + + public static void WriteWarning(string message) + => WriteLine(Prefix("warn: ", Colorize(message, x => Bold + Yellow + x + Reset))); + + public static void WriteInformation(string message) + => WriteLine(Prefix("info: ", message)); + + public static void WriteData(string message) + => WriteLine(Prefix("data: ", Colorize(message, x => Bold + Gray + x + Reset))); + + public static void WriteVerbose(string message) + { + if (IsVerbose) + { + WriteLine(Prefix("verbose: ", Colorize(message, x => Bold + Black + x + Reset))); + } + } + + private static string Prefix(string prefix, string value) + => PrefixOutput + ? string.Join( + Environment.NewLine, + value.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Select(l => prefix + l)) + : value; + + private static void WriteLine(string value) + { + if (NoColor) + { + Console.WriteLine(value); + } + else + { + AnsiConsole.WriteLine(value); + } + } + } +} diff --git a/src/GetDocumentInsider/WrappedException.cs b/src/GetDocumentInsider/WrappedException.cs new file mode 100644 index 0000000000..7cd7bfc0d3 --- /dev/null +++ b/src/GetDocumentInsider/WrappedException.cs @@ -0,0 +1,24 @@ +// 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; + +namespace GetDocument +{ + internal class WrappedException : Exception + { + private readonly string _stackTrace; + + public WrappedException(string type, string message, string stackTrace) + : base(message) + { + Type = type; + _stackTrace = stackTrace; + } + + public string Type { get; } + + public override string ToString() + => _stackTrace; + } +} diff --git a/src/Microsoft.Extensions.ApiDescription.Client/DownloadFile.cs b/src/Microsoft.Extensions.ApiDescription.Client/DownloadFile.cs new file mode 100644 index 0000000000..a1341e0581 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/DownloadFile.cs @@ -0,0 +1,113 @@ +// 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.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Task = System.Threading.Tasks.Task; +using Utilities = Microsoft.Build.Utilities; + +namespace GenerationTasks +{ + /// + /// Downloads a file. + /// + public class DownloadFile : Utilities.Task, ICancelableTask + { + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + + /// + /// The URI to download. + /// + [Required] + public string Uri { get; set; } + + /// + /// Destination for the downloaded file. If the file already exists, it is not re-downloaded unless + /// is true. + /// + [Required] + public string DestinationPath { get; set; } + + /// + /// Should be overwritten. When true, the file is downloaded and its hash + /// compared to the existing file. If those hashes do not match (or does not + /// exist), is overwritten. + /// + public bool Overwrite { get; set; } + + /// + /// The maximum amount of time in seconds to allow for downloading the file. Defaults to 2 minutes. + /// + public int TimeoutSeconds { get; set; } = 60 * 2; + + /// + public void Cancel() => _cts.Cancel(); + + /// + public override bool Execute() => ExecuteAsync().Result; + + public async Task ExecuteAsync() + { + if (string.IsNullOrEmpty(Uri)) + { + Log.LogError("Uri parameter must not be null or empty."); + return false; + } + + if (string.IsNullOrEmpty(Uri)) + { + Log.LogError("DestinationPath parameter must not be null or empty."); + return false; + } + + var builder = new UriBuilder(Uri); + if (!string.Equals(System.Uri.UriSchemeHttp, builder.Scheme, StringComparison.OrdinalIgnoreCase) && + !string.Equals(System.Uri.UriSchemeHttps, builder.Scheme, StringComparison.OrdinalIgnoreCase)) + { + Log.LogError($"{nameof(Uri)} parameter does not have scheme {System.Uri.UriSchemeHttp} or " + + $"{System.Uri.UriSchemeHttps}."); + return false; + } + + await DownloadFileAsync(Uri, DestinationPath, Overwrite, _cts.Token, TimeoutSeconds, Log); + + return !Log.HasLoggedErrors; + } + + private static async Task DownloadFileAsync( + string uri, + string destinationPath, + bool overwrite, + CancellationToken cancellationToken, + int timeoutSeconds, + TaskLoggingHelper log) + { + var destinationExists = File.Exists(destinationPath); + if (destinationExists && !overwrite) + { + log.LogMessage($"Not downloading '{uri}' to overwrite existing file '{destinationPath}'."); + return; + } + + log.LogMessage($"Downloading '{uri}' to '{destinationPath}'."); + + using (var httpClient = new HttpClient + { + }) + { + await DownloadFileCore.DownloadAsync( + uri, + destinationPath, + httpClient, + new LogWrapper(log), + cancellationToken, + timeoutSeconds); + } + } + } +} diff --git a/src/Microsoft.Extensions.ApiDescription.Client/DownloadFileCore.cs b/src/Microsoft.Extensions.ApiDescription.Client/DownloadFileCore.cs new file mode 100644 index 0000000000..bf8156e32f --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/DownloadFileCore.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; + +namespace GenerationTasks +{ + internal static class DownloadFileCore + { + public static async Task DownloadAsync( + string uri, + string destinationPath, + HttpClient httpClient, + ILogWrapper log, + CancellationToken cancellationToken, + int timeoutSeconds) + { + // Timeout if the response has not begun within 1 minute + httpClient.Timeout = TimeSpan.FromMinutes(1); + + var destinationExists = File.Exists(destinationPath); + var reachedCopy = false; + try + { + using (var response = await httpClient.GetAsync(uri, cancellationToken)) + { + response.EnsureSuccessStatusCode(); + cancellationToken.ThrowIfCancellationRequested(); + + using (var responseStreamTask = response.Content.ReadAsStreamAsync()) + { + var finished = await Task.WhenAny( + responseStreamTask, + Task.Delay(TimeSpan.FromSeconds(timeoutSeconds))); + + if (!ReferenceEquals(responseStreamTask, finished)) + { + throw new TimeoutException($"Download failed to complete in {timeoutSeconds} seconds."); + } + + var responseStream = await responseStreamTask; + if (destinationExists) + { + // Check hashes before using the downloaded information. + var downloadHash = GetHash(responseStream); + responseStream.Position = 0L; + + byte[] destinationHash; + using (var destinationStream = File.OpenRead(destinationPath)) + { + destinationHash = GetHash(destinationStream); + } + + var sameHashes = downloadHash.LongLength == destinationHash.LongLength; + for (var i = 0L; sameHashes && i < downloadHash.LongLength; i++) + { + sameHashes = downloadHash[i] == destinationHash[i]; + } + + if (sameHashes) + { + log.LogInformational($"Not overwriting existing and matching file '{destinationPath}'."); + return; + } + } + else + { + // May need to create directory to hold the file. + var destinationDirectory = Path.GetDirectoryName(destinationPath); + if (!(string.IsNullOrEmpty(destinationDirectory) || Directory.Exists(destinationDirectory))) + { + Directory.CreateDirectory(destinationDirectory); + } + } + + // Create or overwrite the destination file. + reachedCopy = true; + using (var outStream = File.Create(destinationPath)) + { + responseStream.CopyTo(outStream); + } + } + } + } + catch (HttpRequestException ex) when (destinationExists) + { + if (ex.InnerException is SocketException socketException) + { + log.LogWarning($"Unable to download {uri}, socket error code '{socketException.SocketErrorCode}'."); + } + else + { + log.LogWarning($"Unable to download {uri}: {ex.Message}"); + } + } + catch (Exception ex) + { + log.LogError($"Downloading '{uri}' failed."); + log.LogError(ex, showStackTrace: true); + if (reachedCopy) + { + File.Delete(destinationPath); + } + } + } + + private static byte[] GetHash(Stream stream) + { + using (var algorithm = SHA256.Create()) + { + return algorithm.ComputeHash(stream); + } + } + } +} diff --git a/src/Microsoft.Extensions.ApiDescription.Client/GetFileReferenceMetadata.cs b/src/Microsoft.Extensions.ApiDescription.Client/GetFileReferenceMetadata.cs new file mode 100644 index 0000000000..6ae50f8b21 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/GetFileReferenceMetadata.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace GenerationTasks +{ + /// + /// Adds or corrects Namespace and OutputPath metadata in ServiceFileReference items. + /// + public class GetFileReferenceMetadata : Task + { + /// + /// Default Namespace metadata value for C# output. + /// + [Required] + public string CSharpNamespace { get; set; } + + /// + /// Default directory for OutputPath values. + /// + [Required] + public string OutputDirectory { get; set; } + + /// + /// Default Namespace metadata value for TypeScript output. + /// + [Required] + public string TypeScriptNamespace { get; set; } + + /// + /// The ServiceFileReference items to update. + /// + [Required] + public ITaskItem[] Inputs { get; set; } + + /// + /// The updated ServiceFileReference items. Will include Namespace and OutputPath metadata. OutputPath metadata + /// will contain full paths. + /// + [Output] + public ITaskItem[] Outputs{ get; set; } + + /// + public override bool Execute() + { + var outputs = new List(Inputs.Length); + foreach (var item in Inputs) + { + var newItem = new TaskItem(item); + outputs.Add(newItem); + + var codeGenerator = item.GetMetadata("CodeGenerator"); + var isTypeScript = codeGenerator.EndsWith("TypeScript", StringComparison.OrdinalIgnoreCase); + + var @namespace = item.GetMetadata("Namespace"); + if (string.IsNullOrEmpty(@namespace)) + { + @namespace = isTypeScript ? CSharpNamespace : TypeScriptNamespace; + newItem.SetMetadata("Namespace", @namespace); + } + + var outputPath = item.GetMetadata("OutputPath"); + if (string.IsNullOrEmpty(outputPath)) + { + var className = item.GetMetadata("ClassName"); + outputPath = className + (isTypeScript ? ".ts" : ".cs"); + } + + outputPath = GetFullPath(outputPath); + newItem.SetMetadata("OutputPath", outputPath); + } + + Outputs = outputs.ToArray(); + + return true; + } + + private string GetFullPath(string path) + { + if (!Path.IsPathRooted(path)) + { + if (!string.IsNullOrEmpty(OutputDirectory)) + { + path = Path.Combine(OutputDirectory, path); + } + + path = Path.GetFullPath(path); + } + + return path; + } + } +} diff --git a/src/Microsoft.Extensions.ApiDescription.Client/GetProjectReferenceMetadata.cs b/src/Microsoft.Extensions.ApiDescription.Client/GetProjectReferenceMetadata.cs new file mode 100644 index 0000000000..e8ac95a819 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/GetProjectReferenceMetadata.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace GenerationTasks +{ + /// + /// Adds or corrects DocumentPath and project-related metadata in ServiceProjectReference items. + /// + public class GetProjectReferenceMetadata : Task + { + /// + /// Default directory for DocumentPath values. + /// + [Required] + public string DocumentDirectory { get; set; } + + /// + /// The ServiceFileReference items to update. + /// + [Required] + public ITaskItem[] Inputs { get; set; } + + /// + /// The updated ServiceFileReference items. Will include Namespace and OutputPath metadata. OutputPath metadata + /// will contain full paths. + /// + [Output] + public ITaskItem[] Outputs{ get; set; } + + /// + public override bool Execute() + { + var outputs = new List(Inputs.Length); + foreach (var item in Inputs) + { + var newItem = new TaskItem(item); + outputs.Add(newItem); + + var codeGenerator = item.GetMetadata("CodeGenerator"); + var isTypeScript = codeGenerator.EndsWith("TypeScript", StringComparison.OrdinalIgnoreCase); + + var outputPath = item.GetMetadata("OutputPath"); + if (string.IsNullOrEmpty(outputPath)) + { + var className = item.GetMetadata("ClassName"); + outputPath = className + (isTypeScript ? ".ts" : ".cs"); + } + + outputPath = GetFullPath(outputPath); + newItem.SetMetadata("OutputPath", outputPath); + } + + Outputs = outputs.ToArray(); + + return true; + } + + private string GetFullPath(string path) + { + if (!Path.IsPathRooted(path)) + { + if (!string.IsNullOrEmpty(DocumentDirectory)) + { + path = Path.Combine(DocumentDirectory, path); + } + + path = Path.GetFullPath(path); + } + + return path; + } + } +} diff --git a/src/Microsoft.Extensions.ApiDescription.Client/GetUriReferenceMetadata.cs b/src/Microsoft.Extensions.ApiDescription.Client/GetUriReferenceMetadata.cs new file mode 100644 index 0000000000..7d922e19dc --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/GetUriReferenceMetadata.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace GenerationTasks +{ + /// + /// Adds or corrects DocumentPath metadata in ServiceUriReference items. + /// + public class GetUriReferenceMetadata : Task + { + /// + /// Default directory for DocumentPath metadata values. + /// + [Required] + public string DocumentDirectory { get; set; } + + /// + /// The ServiceUriReference items to update. + /// + [Required] + public ITaskItem[] Inputs { get; set; } + + /// + /// The updated ServiceUriReference items. Will include DocumentPath metadata with full paths. + /// + [Output] + public ITaskItem[] Outputs{ get; set; } + + /// + public override bool Execute() + { + var outputs = new List(Inputs.Length); + foreach (var item in Inputs) + { + var newItem = new TaskItem(item); + outputs.Add(newItem); + + var documentPath = item.GetMetadata("DocumentPath"); + if (string.IsNullOrEmpty(documentPath)) + { + var uri = item.ItemSpec; + var builder = new UriBuilder(uri); + if (!builder.Uri.IsAbsoluteUri) + { + Log.LogError($"{nameof(Inputs)} item '{uri}' is not an absolute URI."); + return false; + } + + if (!string.Equals(Uri.UriSchemeHttp, builder.Scheme, StringComparison.OrdinalIgnoreCase) && + !string.Equals(Uri.UriSchemeHttps, builder.Scheme, StringComparison.OrdinalIgnoreCase)) + { + Log.LogError($"{nameof(Inputs)} item '{uri}' does not have scheme {Uri.UriSchemeHttp} or " + + $"{Uri.UriSchemeHttps}."); + return false; + } + + var host = builder.Host + .Replace("/", string.Empty) + .Replace("[", string.Empty) + .Replace("]", string.Empty) + .Replace(':', '_'); + var path = builder.Path + .Replace("!", string.Empty) + .Replace("'", string.Empty) + .Replace("$", string.Empty) + .Replace("%", string.Empty) + .Replace("&", string.Empty) + .Replace("(", string.Empty) + .Replace(")", string.Empty) + .Replace("*", string.Empty) + .Replace("@", string.Empty) + .Replace("~", string.Empty) + .Replace('/', '_') + .Replace(':', '_') + .Replace(';', '_') + .Replace('+', '_') + .Replace('=', '_'); + + documentPath = host + path; + if (char.IsLower(documentPath[0])) + { + documentPath = char.ToUpper(documentPath[0]) + documentPath.Substring(startIndex: 1); + } + + if (!documentPath.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + documentPath = $"{documentPath}.json"; + } + } + + documentPath = GetFullPath(documentPath); + newItem.SetMetadata("DocumentPath", documentPath); + } + + Outputs = outputs.ToArray(); + + return true; + } + + private string GetFullPath(string path) + { + if (!Path.IsPathRooted(path)) + { + if (!string.IsNullOrEmpty(DocumentDirectory)) + { + path = Path.Combine(DocumentDirectory, path); + } + + path = Path.GetFullPath(path); + } + + return path; + } + } +} diff --git a/src/Microsoft.Extensions.ApiDescription.Client/ILogWrapper.cs b/src/Microsoft.Extensions.ApiDescription.Client/ILogWrapper.cs new file mode 100644 index 0000000000..7c773ab359 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/ILogWrapper.cs @@ -0,0 +1,50 @@ +using System; + +namespace GenerationTasks +{ + internal interface ILogWrapper + { + /// + /// Logs specified informational . Implementations should be thread safe. + /// + /// The message to log. + /// Optional arguments for formatting the string. + /// + /// Thrown when is . + /// + void LogInformational(string message, params object[] messageArgs); + + /// + /// Logs a warning with the specified . Implementations should be thread safe. + /// + /// The message to log. + /// Optional arguments for formatting the string. + /// + /// Thrown when is . + /// + void LogWarning(string message, params object[] messageArgs); + + /// + /// Logs an error with the specified . Implementations should be thread safe. + /// + /// The message to log. + /// Optional arguments for formatting the string. + /// + /// Thrown when is . + /// + void LogError(string message, params object[] messageArgs); + + /// + /// Logs an error with the message and (optionally) the stack trace of the given . + /// Implementations should be thread safe. + /// + /// The to log. + /// + /// If , append stack trace to 's message. + /// + /// + /// Thrown when is . + /// + void LogError(Exception exception, bool showStackTrace); + } +} diff --git a/src/Microsoft.Extensions.ApiDescription.Client/LogWrapper.cs b/src/Microsoft.Extensions.ApiDescription.Client/LogWrapper.cs new file mode 100644 index 0000000000..75ae1407a8 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/LogWrapper.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Build.Utilities; + +namespace GenerationTasks +{ + internal class LogWrapper : ILogWrapper + { + private readonly TaskLoggingHelper _log; + + public LogWrapper(TaskLoggingHelper log) + { + _log = log; + } + + public void LogError(string message, params object[] messageArgs) + { + _log.LogError(message, messageArgs); + } + + public void LogError(Exception exception, bool showStackTrace) + { + _log.LogErrorFromException(exception, showStackTrace); + } + + public void LogInformational(string message, params object[] messageArgs) + { + _log.LogMessage(message, messageArgs); + } + + public void LogWarning(string message, params object[] messageArgs) + { + _log.LogWarning(message, messageArgs); + } + } +} diff --git a/src/Microsoft.Extensions.ApiDescription.Client/Microsoft.Extensions.ApiDescription.Client.csproj b/src/Microsoft.Extensions.ApiDescription.Client/Microsoft.Extensions.ApiDescription.Client.csproj new file mode 100644 index 0000000000..5dab6ea1aa --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/Microsoft.Extensions.ApiDescription.Client.csproj @@ -0,0 +1,46 @@ + + + + $(GenerateNuspecDependsOn);PopulateNuspec + + + true + + MSBuild tasks and targets for code generation + false + false + $(MSBuildProjectName).nuspec + Build Tasks;msbuild;DownloadFile;GetFilenameFromUri;code generation + GenerationTasks + netstandard2.0;net461 + + + + + + + + + + + + + + + id=$(PackageId); + authors=$(Authors); + configuration=$(Configuration); + copyright=$(Copyright); + description=$(PackageDescription); + iconUrl=$(PackageIconUrl); + licenseUrl=$(PackageLicenseUrl); + owners=$(Company); + projectUrl=$(PackageProjectUrl); + repositoryCommit=$(RepositoryCommit); + repositoryUrl=$(RepositoryUrl); + tags=$(PackageTags.Replace(';', ' ')); + version=$(PackageVersion); + + + + diff --git a/src/Microsoft.Extensions.ApiDescription.Client/Microsoft.Extensions.ApiDescription.Client.nuspec b/src/Microsoft.Extensions.ApiDescription.Client/Microsoft.Extensions.ApiDescription.Client.nuspec new file mode 100644 index 0000000000..5b840bd379 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/Microsoft.Extensions.ApiDescription.Client.nuspec @@ -0,0 +1,28 @@ + + + + $id$ + $authors$ + $copyright$ + $description$ + true + $iconUrl$ + $licenseUrl$ + 2.8 + $owners$ + $projectUrl$ + + false + $tags$ + $version$ + + + + + + + + + + + diff --git a/src/Microsoft.Extensions.ApiDescription.Client/ServiceProjectReferenceMetadata.targets b/src/Microsoft.Extensions.ApiDescription.Client/ServiceProjectReferenceMetadata.targets new file mode 100644 index 0000000000..5ed88a3f6c --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/ServiceProjectReferenceMetadata.targets @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.Extensions.ApiDescription.Client/build/GenerationTasks.props b/src/Microsoft.Extensions.ApiDescription.Client/build/GenerationTasks.props new file mode 100644 index 0000000000..dc67dca96a --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/build/GenerationTasks.props @@ -0,0 +1,133 @@ + + + + + <_GenerationTasksAssemblyTarget Condition="'$(MSBuildRuntimeType)' == 'Core'">netstandard2.0 + <_GenerationTasksAssemblyTarget Condition="'$(MSBuildRuntimeType)' != 'Core'">net461 + <_GenerationTasksAssemblyPath>$(MSBuildThisFileDirectory)/../tasks/$(_GenerationTasksAssemblyTarget)/GenerationTasks.dll + <_GenerationTasksAssemblyTarget /> + + + + + + + + true + $([MSBuild]::EnsureTrailingSlash('$(ServiceProjectReferenceDirectory)')) + + true + $([MSBuild]::EnsureTrailingSlash('$(ServiceUriReferenceDirectory)')) + + true + $([MSBuild]::EnsureTrailingSlash('$(ServiceFileReferenceDirectory)')) + $(RootNamespace) + $(RootNamespace) + + + _DefaultDocumentGenerator_GetMetadata; + _DefaultDocumentGenerator_Core; + _DefaultDocumentGenerator_SetMetadata + + + _ServiceProjectReferenceGenerator_GetTargetFramework; + _ServiceProjectReferenceGenerator_GetProjectTargetPath; + _ServiceProjectReferenceGenerator_Restore; + _ServiceProjectReferenceGenerator_Build; + _ServiceProjectReferenceGenerator_Core + + + _ServiceUriReferenceGenerator_GetMetadata; + _ServiceUriReferenceGenerator_Core + + + _CheckServiceReferences; + ServiceProjectReferenceGenerator; + ServiceUriReferenceGenerator; + _ServiceFileReferenceGenerator_GetMetadata; + _ServiceFileReferenceGenerator_Core + + + + + + + + Default + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %(Filename)Client + + + + + + + + + diff --git a/src/Microsoft.Extensions.ApiDescription.Client/build/GenerationTasks.targets b/src/Microsoft.Extensions.ApiDescription.Client/build/GenerationTasks.targets new file mode 100644 index 0000000000..7c70d7d279 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/build/GenerationTasks.targets @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + <_FullPath>%(ServiceProjectReference.FullPath) + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + <_TargetFrameworks>%(_Temporary.TargetFrameworks) + <_TargetFramework>$(_TargetFrameworks.Split(';')[0]) + + + + $(_TargetFramework) + + <_Temporary Remove="@(_Temporary)" /> + + + + <_FullPath /> + <_TargetFramework /> + <_TargetFrameworks /> + + + + + + + + <_FullPath>%(ServiceProjectReference.FullPath) + <_TargetFramework>%(ServiceProjectReference.TargetFramework) + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + <_ProjectTargetPath>%(_Temporary.FullPath) + + + + $(_ProjectTargetPath) + + <_Temporary Remove="@(_Temporary)" /> + + + + <_FullPath /> + <_ProjectTargetPath /> + <_TargetFramework /> + + + + + + + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + <_Temporary Include="@(ServiceProjectReference -> WithMetadataValue('DocumentGenerator', 'Default'))" /> + + + + + + + + + + <_Command>dotnet getdocument --configuration $(Configuration) --no-build + + + <_Temporary Update="@(_Temporary)"> + $(DefaultDocumentGeneratorDefaultOptions) + $(_Command) --project %(FullPath) --output %(DocumentPath) --framework %(TargetFramework) + + <_Temporary Update="@(_Temporary)"> + %(Command) --uri %(_Temporary.Uri) + + <_Temporary Update="@(_Temporary)"> + %(Command) --service %(_Temporary.Service) --method %(_Temporary.Method) + + <_Temporary Update="@(_Temporary)"> + %(Command) %(_Temporary.Options) + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + + + diff --git a/src/Microsoft.Extensions.ApiDescription.Client/build/NSwagServiceReference.props b/src/Microsoft.Extensions.ApiDescription.Client/build/NSwagServiceReference.props new file mode 100644 index 0000000000..4269fdd11c --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/build/NSwagServiceReference.props @@ -0,0 +1,20 @@ + + + + + _NSwagDocumentGenerator_GetMetadata; + _NSwagDocumentGenerator_Core; + _NSwagDocumentGenerator_SetMetadata + + + _NSwagCSharpCodeGenerator_GetMetadata; + _NSwagCSharpCodeGenerator_Core; + _NSwagCSharpCodeGenerator_SetMetadata + + + _NSwagTypeScriptCodeGenerator_GetMetadata; + _NSwagTypeScriptCodeGenerator_Core; + _NSwagTypeScriptCodeGenerator_SetMetadata + + + diff --git a/src/Microsoft.Extensions.ApiDescription.Client/build/NSwagServiceReference.targets b/src/Microsoft.Extensions.ApiDescription.Client/build/NSwagServiceReference.targets new file mode 100644 index 0000000000..e73cd8d641 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/build/NSwagServiceReference.targets @@ -0,0 +1,131 @@ + + + + + + + <_NSwagTemporary Remove="@(_NSwagTemporary)" /> + <_NSwagTemporary Include="@(ServiceProjectReference -> WithMetadataValue('DocumentGenerator', 'NSwag'))"> + $(NSwagDocumentGeneratorDefaultOptions) + + + + + + + + <_Command>$(NSwagExe_Core21) aspnetcore2swagger /Configuration:$(Configuration) /NoBuild:true + + + + + + + <_Command /> + + + + + + + + + <_NSwagTemporary Remove="@(_NSwagTemporary)" /> + + + + + + + + + + <_NSwagTemporary Remove="@(_NSwagTemporary)" /> + <_NSwagTemporary Include="@(ServiceFileReference -> WithMetadataValue('CodeGenerator', 'NSwagCSharp'))"> + $(NSwagCSharpCodeGeneratorDefaultOptions) + + <_NSwagTemporary Update="@(_NSwagTemporary)"> + $(ServiceFileReferenceDirectory)%(ClassName).cs + + + + + + + <_Command>$(NSwagExe_Core21) swagger2csclient /namespace:$(NSwagCSharpCodeGeneratorNamespace) + + + + + + + <_Command /> + + + + + + + + + + + <_NSwagTemporary Remove="@(_NSwagTemporary)" /> + + + + + + + + + + <_NSwagTemporary Remove="@(_NSwagTemporary)" /> + <_NSwagTemporary Include="@(ServiceFileReference -> WithMetadataValue('CodeGenerator', 'NSwagTypeScript'))"> + $(NSwagTypeScriptCodeGeneratorDefaultOptions) + + <_NSwagTemporary Update="@(_NSwagTemporary)"> + $(ServiceFileReferenceDirectory)%(ClassName).ts + + + + + + + <_Command>$(NSwagExe_Core21) swagger2tsclient /namespace:$(NSwagTypeScriptCodeGeneratorNamespace) + + + + + + + <_Command /> + + + + + + + + + <_NSwagTemporary Remove="@(_NSwagTemporary)" /> + + + + + diff --git a/src/Microsoft.Extensions.ApiDescription.Client/buildMultiTargeting/GenerationTasks.targets b/src/Microsoft.Extensions.ApiDescription.Client/buildMultiTargeting/GenerationTasks.targets new file mode 100644 index 0000000000..5e73fd66e1 --- /dev/null +++ b/src/Microsoft.Extensions.ApiDescription.Client/buildMultiTargeting/GenerationTasks.targets @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/dotnet-getdocument/Commands/InvokeCommand.cs b/src/dotnet-getdocument/Commands/InvokeCommand.cs new file mode 100644 index 0000000000..cf3308ec46 --- /dev/null +++ b/src/dotnet-getdocument/Commands/InvokeCommand.cs @@ -0,0 +1,241 @@ +// 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 System.Linq; +using System.Runtime.Versioning; +using GetDocument.Properties; +using Microsoft.DotNet.Cli.CommandLine; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace GetDocument.Commands +{ + internal class InvokeCommand : HelpCommandBase + { + private const string InsideManName = "GetDocument.Insider"; + + private CommandOption _configuration; + private CommandOption _framework; + private CommandOption _msbuildprojectextensionspath; + private CommandOption _output; + private CommandOption _project; + private CommandOption _runtime; + private IList _args; + + public override void Configure(CommandLineApplication command) + { + var options = new ProjectOptions(); + options.Configure(command); + + _project = options.Project; + _framework = options.Framework; + _configuration = options.Configuration; + _runtime = options.Runtime; + _msbuildprojectextensionspath = options.MSBuildProjectExtensionsPath; + + _output = command.Option("--output ", Resources.OutputDescription); + command.VersionOption("--version", ProductInfo.GetVersion); + _args = command.RemainingArguments; + + base.Configure(command); + } + + protected override int Execute() + { + var projectFile = FindProjects( + _project.Value(), + Resources.NoProject, + Resources.MultipleProjects); + Reporter.WriteVerbose(Resources.UsingProject(projectFile)); + + var project = Project.FromFile( + projectFile, + _msbuildprojectextensionspath.Value(), + _framework.Value(), + _configuration.Value(), + _runtime.Value()); + if (!File.Exists(project.AssemblyPath)) + { + throw new CommandException(Resources.MustBuild); + } + + var thisPath = Path.GetFullPath(Path.GetDirectoryName(typeof(InvokeCommand).Assembly.Location)); + + string executable = null; + var cleanupExecutable = false; + try + { + string toolsDirectory; + var args = new List(); + var targetFramework = new FrameworkName(project.TargetFrameworkMoniker); + switch (targetFramework.Identifier) + { + case ".NETFramework": + cleanupExecutable = true; + executable = Path.Combine(project.OutputPath, InsideManName + ".exe"); + toolsDirectory = Path.Combine( + thisPath, + project.PlatformTarget == "x86" ? "net461-x86" : "net461"); + + var executableSource = Path.Combine(toolsDirectory, InsideManName + ".exe"); + File.Copy(executableSource, executable, overwrite: true); + + if (!string.IsNullOrEmpty(project.ConfigPath)) + { + File.Copy(project.ConfigPath, executable + ".config", overwrite: true); + } + break; + + case ".NETCoreApp": + executable = "dotnet"; + toolsDirectory = Path.Combine(thisPath, "netcoreapp2.0"); + + if (targetFramework.Version < new Version(2, 0)) + { + throw new CommandException( + Resources.NETCoreApp1Project(project.Name, targetFramework.Version)); + } + + args.Add("exec"); + args.Add("--depsFile"); + args.Add(project.DepsPath); + + if (!string.IsNullOrEmpty(project.AssetsPath)) + { + using (var reader = new JsonTextReader(File.OpenText(project.AssetsPath))) + { + var projectAssets = JToken.ReadFrom(reader); + var packageFolders = projectAssets["packageFolders"] + .Children() + .Select(p => p.Name); + + foreach (var packageFolder in packageFolders) + { + args.Add("--additionalProbingPath"); + args.Add(packageFolder.TrimEnd(Path.DirectorySeparatorChar)); + } + } + } + + if (File.Exists(project.RuntimeConfigPath)) + { + args.Add("--runtimeConfig"); + args.Add(project.RuntimeConfigPath); + } + else if (!string.IsNullOrEmpty(project.RuntimeFrameworkVersion)) + { + args.Add("--fx-version"); + args.Add(project.RuntimeFrameworkVersion); + } + + args.Add(Path.Combine(toolsDirectory, InsideManName + ".dll")); + break; + + case ".NETStandard": + throw new CommandException(Resources.NETStandardProject(project.Name)); + + default: + throw new CommandException( + Resources.UnsupportedFramework(project.Name, targetFramework.Identifier)); + } + + args.AddRange(_args); + args.Add("--assembly"); + args.Add(project.AssemblyPath); + args.Add("--tools-directory"); + args.Add(toolsDirectory); + + if (!(args.Contains("--method") || string.IsNullOrEmpty(project.DefaultMethod))) + { + args.Add("--method"); + args.Add(project.DefaultMethod); + } + + if (!(args.Contains("--service") || string.IsNullOrEmpty(project.DefaultService))) + { + args.Add("--service"); + args.Add(project.DefaultService); + } + + if (!(args.Contains("--uri") || string.IsNullOrEmpty(project.DefaultUri))) + { + args.Add("--uri"); + args.Add(project.DefaultUri); + } + + if (_output.HasValue()) + { + args.Add("--output"); + args.Add(Path.GetFullPath(_output.Value())); + } + + if (Reporter.IsVerbose) + { + args.Add("--verbose"); + } + + if (Reporter.NoColor) + { + args.Add("--no-color"); + } + + if (Reporter.PrefixOutput) + { + args.Add("--prefix-output"); + } + + return Exe.Run(executable, args, project.Directory); + } + finally + { + if (cleanupExecutable && !string.IsNullOrEmpty(executable)) + { + File.Delete(executable); + File.Delete(executable + ".config"); + } + } + } + + private static string FindProjects( + string path, + string errorWhenNoProject, + string errorWhenMultipleProjects) + { + var specified = true; + if (path == null) + { + specified = false; + path = Directory.GetCurrentDirectory(); + } + else if (!Directory.Exists(path)) // It's not a directory + { + return path; + } + + var projectFiles = Directory + .EnumerateFiles(path, "*.*proj", SearchOption.TopDirectoryOnly) + .Where(f => !string.Equals(Path.GetExtension(f), ".xproj", StringComparison.OrdinalIgnoreCase)) + .Take(2) + .ToList(); + if (projectFiles.Count == 0) + { + throw new CommandException( + specified + ? Resources.NoProjectInDirectory(path) + : errorWhenNoProject); + } + if (projectFiles.Count != 1) + { + throw new CommandException( + specified + ? Resources.MultipleProjectsInDirectory(path) + : errorWhenMultipleProjects); + } + + return projectFiles[0]; + } + } +} diff --git a/src/dotnet-getdocument/Exe.cs b/src/dotnet-getdocument/Exe.cs new file mode 100644 index 0000000000..0c8f7d89ae --- /dev/null +++ b/src/dotnet-getdocument/Exe.cs @@ -0,0 +1,118 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace GetDocument +{ + internal static class Exe + { + public static int Run( + string executable, + IReadOnlyList args, + string workingDirectory = null, + bool interceptOutput = false) + { + var arguments = ToArguments(args); + + Reporter.WriteVerbose(executable + " " + arguments); + + var startInfo = new ProcessStartInfo + { + FileName = executable, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = interceptOutput + }; + if (workingDirectory != null) + { + startInfo.WorkingDirectory = workingDirectory; + } + + var process = Process.Start(startInfo); + + if (interceptOutput) + { + string line; + while ((line = process.StandardOutput.ReadLine()) != null) + { + Reporter.WriteVerbose(line); + } + } + + process.WaitForExit(); + + return process.ExitCode; + } + + private static string ToArguments(IReadOnlyList args) + { + var builder = new StringBuilder(); + for (var i = 0; i < args.Count; i++) + { + if (i != 0) + { + builder.Append(" "); + } + + if (args[i].IndexOf(' ') == -1) + { + builder.Append(args[i]); + + continue; + } + + builder.Append("\""); + + var pendingBackslashs = 0; + for (var j = 0; j < args[i].Length; j++) + { + switch (args[i][j]) + { + case '\"': + if (pendingBackslashs != 0) + { + builder.Append('\\', pendingBackslashs * 2); + pendingBackslashs = 0; + } + builder.Append("\\\""); + break; + + case '\\': + pendingBackslashs++; + break; + + default: + if (pendingBackslashs != 0) + { + if (pendingBackslashs == 1) + { + builder.Append("\\"); + } + else + { + builder.Append('\\', pendingBackslashs * 2); + } + + pendingBackslashs = 0; + } + + builder.Append(args[i][j]); + break; + } + } + + if (pendingBackslashs != 0) + { + builder.Append('\\', pendingBackslashs * 2); + } + + builder.Append("\""); + } + + return builder.ToString(); + } + } +} diff --git a/src/dotnet-getdocument/Program.cs b/src/dotnet-getdocument/Program.cs new file mode 100644 index 0000000000..495573a776 --- /dev/null +++ b/src/dotnet-getdocument/Program.cs @@ -0,0 +1,43 @@ +// 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 GetDocument.Commands; +using GetDocument.Properties; +using Microsoft.DotNet.Cli.CommandLine; + +namespace GetDocument +{ + internal static class Program + { + private static int Main(string[] args) + { + var app = new CommandLineApplication(throwOnUnexpectedArg: false) + { + FullName = Resources.CommandFullName, + }; + + new InvokeCommand().Configure(app); + + try + { + return app.Execute(args); + } + catch (Exception ex) + { + if (ex is CommandException || ex is CommandParsingException) + { + Reporter.WriteVerbose(ex.ToString()); + } + else + { + Reporter.WriteInformation(ex.ToString()); + } + + Reporter.WriteError(ex.Message); + + return 1; + } + } + } +} diff --git a/src/dotnet-getdocument/Project.cs b/src/dotnet-getdocument/Project.cs new file mode 100644 index 0000000000..274960a08b --- /dev/null +++ b/src/dotnet-getdocument/Project.cs @@ -0,0 +1,228 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using GetDocument.Properties; +using IODirectory = System.IO.Directory; + +namespace GetDocument +{ + internal class Project + { + private const string MSBuildResourceName = "GetDocument.ServiceProjectReferenceMetadata"; + + private Project() + { + } + + public string AssemblyName { get; private set; } + + public string AssemblyPath { get; private set; } + + public string AssetsPath { get; private set; } + + public string Configuration { get; private set; } + + public string ConfigPath { get; private set; } + + public string DefaultDocumentName { get; private set; } + + public string DefaultMethod { get; private set; } + + public string DefaultService { get; private set; } + + public string DefaultUri { get; private set; } + + public string DepsPath { get; private set; } + + public string Directory { get; private set; } + + public string ExtensionsPath { get; private set; } + + public string Name { get; private set; } + + public string OutputPath { get; private set; } + + public string Platform { get; private set; } + + public string PlatformTarget { get; private set; } + + public string RuntimeConfigPath { get; private set; } + + public string RuntimeFrameworkVersion { get; private set; } + + public string TargetFramework { get; private set; } + + public string TargetFrameworkMoniker { get; private set; } + + public static Project FromFile( + string file, + string buildExtensionsDirectory, + string framework = null, + string configuration = null, + string runtime = null) + { + Debug.Assert(!string.IsNullOrEmpty(file), "file is null or empty."); + + if (string.IsNullOrEmpty(buildExtensionsDirectory)) + { + buildExtensionsDirectory = Path.Combine(Path.GetDirectoryName(file), "obj"); + } + + IODirectory.CreateDirectory(buildExtensionsDirectory); + + var assembly = typeof(Project).Assembly; + var propsPath = Path.Combine( + buildExtensionsDirectory, + Path.GetFileName(file) + ".ServiceProjectReferenceMetadata.props"); + using (var input = assembly.GetManifestResourceStream($"{MSBuildResourceName}.props")) + { + using (var output = File.OpenWrite(propsPath)) + { + Reporter.WriteVerbose(Resources.WritingFile(propsPath)); + input.CopyTo(output); + } + } + + var targetsPath = Path.ChangeExtension(propsPath, ".targets"); + using (var input = assembly.GetManifestResourceStream($"{MSBuildResourceName}.targets")) + { + using (var output = File.OpenWrite(targetsPath)) + { + // NB: Copy always in case it changes + Reporter.WriteVerbose(Resources.WritingFile(targetsPath)); + input.CopyTo(output); + } + } + + IDictionary metadata; + var metadataPath = Path.GetTempFileName(); + try + { + var propertyArg = "/property:ServiceProjectReferenceMetadataPath=" + metadataPath; + if (!string.IsNullOrEmpty(framework)) + { + propertyArg += ";TargetFramework=" + framework; + } + if (!string.IsNullOrEmpty(configuration)) + { + propertyArg += ";Configuration=" + configuration; + } + if (!string.IsNullOrEmpty(runtime)) + { + propertyArg += ";RuntimeIdentifier=" + runtime; + } + + var args = new List + { + "msbuild", + "/target:WriteServiceProjectReferenceMetadata", + propertyArg, + "/verbosity:quiet", + "/nologo" + }; + + if (!string.IsNullOrEmpty(file)) + { + args.Add(file); + } + + var exitCode = Exe.Run("dotnet", args); + if (exitCode != 0) + { + throw new CommandException(Resources.GetMetadataFailed); + } + + metadata = File.ReadLines(metadataPath).Select(l => l.Split(new[] { ':' }, 2)) + .ToDictionary(s => s[0], s => s[1].TrimStart()); + } + finally + { + File.Delete(propsPath); + File.Delete(metadataPath); + File.Delete(targetsPath); + } + + var project = new Project + { + AssemblyName = metadata[nameof(AssemblyName)], + AssemblyPath = metadata[nameof(AssemblyPath)], + AssetsPath = metadata[nameof(AssetsPath)], + Configuration = metadata[nameof(Configuration)], + DefaultDocumentName = metadata[nameof(DefaultDocumentName)], + DefaultMethod = metadata[nameof(DefaultMethod)], + DefaultService = metadata[nameof(DefaultService)], + DefaultUri = metadata[nameof(DefaultUri)], + DepsPath = metadata[nameof(DepsPath)], + Directory = metadata[nameof(Directory)], + ExtensionsPath = metadata[nameof(ExtensionsPath)], + Name = metadata[nameof(Name)], + OutputPath = metadata[nameof(OutputPath)], + Platform = metadata[nameof(Platform)], + PlatformTarget = metadata[nameof(PlatformTarget)] ?? metadata[nameof(Platform)], + RuntimeConfigPath = metadata[nameof(RuntimeConfigPath)], + RuntimeFrameworkVersion = metadata[nameof(RuntimeFrameworkVersion)], + TargetFramework = metadata[nameof(TargetFramework)], + TargetFrameworkMoniker = metadata[nameof(TargetFrameworkMoniker)], + }; + + if (string.IsNullOrEmpty(project.AssemblyPath)) + { + throw new CommandException(Resources.GetMetadataValueFailed(nameof(AssemblyPath), "TargetPath")); + } + + if (string.IsNullOrEmpty(project.Directory)) + { + throw new CommandException(Resources.GetMetadataValueFailed(nameof(Directory), "ProjectDir")); + } + + if (string.IsNullOrEmpty(project.OutputPath)) + { + throw new CommandException(Resources.GetMetadataValueFailed(nameof(OutputPath), "OutDir")); + } + + if (!Path.IsPathRooted(project.Directory)) + { + project.Directory = Path.GetFullPath(Path.Combine(IODirectory.GetCurrentDirectory(), project.Directory)); + } + + if (!Path.IsPathRooted(project.AssemblyPath)) + { + project.AssemblyPath = Path.GetFullPath(Path.Combine(project.Directory, project.AssemblyPath)); + } + + if (!Path.IsPathRooted(project.OutputPath)) + { + project.OutputPath = Path.GetFullPath(Path.Combine(project.Directory, project.OutputPath)); + } + + // Some document generation tools support non-ASP.NET Core projects. + // Thus any of the remaining properties may be empty. + if (!(string.IsNullOrEmpty(project.AssetsPath) || Path.IsPathRooted(project.AssetsPath))) + { + project.AssetsPath = Path.GetFullPath(Path.Combine(project.Directory, project.AssetsPath)); + } + + var configPath = $"{project.AssemblyPath}.config"; + if (File.Exists(configPath)) + { + project.ConfigPath = configPath; + } + + if (!(string.IsNullOrEmpty(project.DepsPath) || Path.IsPathRooted(project.DepsPath))) + { + project.DepsPath = Path.GetFullPath(Path.Combine(project.Directory, project.DepsPath)); + } + + if (!(string.IsNullOrEmpty(project.RuntimeConfigPath) || Path.IsPathRooted(project.RuntimeConfigPath))) + { + project.RuntimeConfigPath = Path.GetFullPath(Path.Combine(project.Directory, project.RuntimeConfigPath)); + } + + return project; + } + } +} diff --git a/src/dotnet-getdocument/ProjectOptions.cs b/src/dotnet-getdocument/ProjectOptions.cs new file mode 100644 index 0000000000..bca160eeaa --- /dev/null +++ b/src/dotnet-getdocument/ProjectOptions.cs @@ -0,0 +1,30 @@ +// 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 GetDocument.Properties; +using Microsoft.DotNet.Cli.CommandLine; + +namespace GetDocument +{ + internal class ProjectOptions + { + public CommandOption Project { get; private set; } + + public CommandOption Framework { get; private set; } + + public CommandOption Configuration { get; private set; } + + public CommandOption Runtime { get; private set; } + + public CommandOption MSBuildProjectExtensionsPath { get; private set; } + + public void Configure(CommandLineApplication command) + { + Project = command.Option("-p|--project ", Resources.ProjectDescription); + Framework = command.Option("--framework ", Resources.FrameworkDescription); + Configuration = command.Option("--configuration ", Resources.ConfigurationDescription); + Runtime = command.Option("--runtime ", Resources.RuntimeDescription); + MSBuildProjectExtensionsPath = command.Option("--msbuildprojectextensionspath ", Resources.ProjectExtensionsDescription); + } + } +} diff --git a/src/dotnet-getdocument/Properties/Resources.Designer.cs b/src/dotnet-getdocument/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..3ee7f0ba79 --- /dev/null +++ b/src/dotnet-getdocument/Properties/Resources.Designer.cs @@ -0,0 +1,179 @@ +// + +using System; +using System.Reflection; +using System.Resources; +using JetBrains.Annotations; + +namespace GetDocument.Properties +{ + /// + /// This API supports the GetDocument infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("GetDocument.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The configuration to use. + /// + public static string ConfigurationDescription + => GetString("ConfigurationDescription"); + + /// + /// dotnet getdocument + /// + public static string CommandFullName + => GetString("CommandFullName"); + + /// + /// The target framework. + /// + public static string FrameworkDescription + => GetString("FrameworkDescription"); + + /// + /// Unable to retrieve project metadata. If you are using custom BaseIntermediateOutputPath or MSBuildProjectExtensionsPath values, use the --msbuildprojectextensionspath option. + /// + public static string GetMetadataFailed + => GetString("GetMetadataFailed"); + + /// + /// More than one project was found in the current working directory. Use the --project option. + /// + public static string MultipleProjects + => GetString("MultipleProjects"); + + /// + /// More than one project was found in directory '{projectDirectory}'. Specify one using its file name. + /// + public static string MultipleProjectsInDirectory([CanBeNull] object projectDirectory) + => string.Format( + GetString("MultipleProjectsInDirectory", nameof(projectDirectory)), + projectDirectory); + + /// + /// Project '{Project}' targets framework '.NETCoreApp' version '{targetFrameworkVersion}'. This version of the GetDocument Command-line Tool only supports version 2.0 or higher. + /// + public static string NETCoreApp1Project([CanBeNull] object Project, [CanBeNull] object targetFrameworkVersion) + => string.Format( + GetString("NETCoreApp1Project", nameof(Project), nameof(targetFrameworkVersion)), + Project, targetFrameworkVersion); + + /// + /// Project '{Project}' targets framework '.NETStandard'. There is no runtime associated with this framework, and projects targeting it cannot be executed directly. To use the GetDocument Command-line Tool with this project, add an executable project targeting .NET Core or .NET Framework that references this project and specify it using the --project option; or, update this project to target .NET Core and / or .NET Framework. + /// + public static string NETStandardProject([CanBeNull] object Project) + => string.Format( + GetString("NETStandardProject", nameof(Project)), + Project); + + /// + /// Do not colorize output. + /// + public static string NoColorDescription + => GetString("NoColorDescription"); + + /// + /// No project was found. Change the current working directory or use the --project option. + /// + public static string NoProject + => GetString("NoProject"); + + /// + /// No project was found in directory '{projectDirectory}'. + /// + public static string NoProjectInDirectory([CanBeNull] object projectDirectory) + => string.Format( + GetString("NoProjectInDirectory", nameof(projectDirectory)), + projectDirectory); + + /// + /// Prefix output with level. + /// + public static string PrefixDescription + => GetString("PrefixDescription"); + + /// + /// The project to use. + /// + public static string ProjectDescription + => GetString("ProjectDescription"); + + /// + /// The MSBuild project extensions path. Defaults to "obj". + /// + public static string ProjectExtensionsDescription + => GetString("ProjectExtensionsDescription"); + + /// + /// The runtime identifier to use. + /// + public static string RuntimeDescription + => GetString("RuntimeDescription"); + + /// + /// Project '{Project}' targets framework '{targetFramework}'. The GetDocument Command-line Tool does not support this framework. + /// + public static string UnsupportedFramework([CanBeNull] object Project, [CanBeNull] object targetFramework) + => string.Format( + GetString("UnsupportedFramework", nameof(Project), nameof(targetFramework)), + Project, targetFramework); + + /// + /// Using project '{project}'. + /// + public static string UsingProject([CanBeNull] object project) + => string.Format( + GetString("UsingProject", nameof(project)), + project); + + /// + /// Show verbose output. + /// + public static string VerboseDescription + => GetString("VerboseDescription"); + + /// + /// Writing '{file}'... + /// + public static string WritingFile([CanBeNull] object file) + => string.Format( + GetString("WritingFile", nameof(file)), + file); + + /// + /// Project output not found and --no-build was specified. Project must be up-to-date when using the --no-build option. + /// + public static string MustBuild + => GetString("MustBuild"); + + /// + /// The file to write the result to. + /// + public static string OutputDescription + => GetString("OutputDescription"); + + /// + /// Unable to retrieve '{properrty}' project metadata. Ensure '{msbuildProperty}' is set. + /// + public static string GetMetadataValueFailed([CanBeNull] object properrty, [CanBeNull] object msbuildProperty) + => string.Format( + GetString("GetMetadataValueFailed", nameof(properrty), nameof(msbuildProperty)), + properrty, msbuildProperty); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + + return value; + } + } +} + diff --git a/src/dotnet-getdocument/Properties/Resources.Designer.tt b/src/dotnet-getdocument/Properties/Resources.Designer.tt new file mode 100644 index 0000000000..3f636a4db5 --- /dev/null +++ b/src/dotnet-getdocument/Properties/Resources.Designer.tt @@ -0,0 +1,6 @@ +<# + Session["ResourceFile"] = "Resources.resx"; + Session["AccessModifier"] = "internal"; + Session["NoDiagnostics"] = true; +#> +<#@ include file="..\..\tools\Resources.tt" #> diff --git a/src/dotnet-getdocument/Properties/Resources.resx b/src/dotnet-getdocument/Properties/Resources.resx new file mode 100644 index 0000000000..16e16b8086 --- /dev/null +++ b/src/dotnet-getdocument/Properties/Resources.resx @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The configuration to use. + + + dotnet getdocument + + + The target framework. + + + Unable to retrieve project metadata. If you are using custom BaseIntermediateOutputPath or MSBuildProjectExtensionsPath values, use the --msbuildprojectextensionspath option. + + + More than one project was found in the current working directory. Use the --project option. + + + More than one project was found in directory '{projectDirectory}'. Specify one using its file name. + + + Project '{Project}' targets framework '.NETCoreApp' version '{targetFrameworkVersion}'. This version of the GetDocument Command-line Tool only supports version 2.0 or higher. + + + Project '{Project}' targets framework '.NETStandard'. There is no runtime associated with this framework, and projects targeting it cannot be executed directly. To use the GetDocument Command-line Tool with this project, add an executable project targeting .NET Core or .NET Framework that references this project and specify it using the --project option; or, update this project to target .NET Core and / or .NET Framework. + + + Do not colorize output. + + + No project was found. Change the current working directory or use the --project option. + + + No project was found in directory '{projectDirectory}'. + + + Prefix output with level. + + + The project to use. + + + The MSBuild project extensions path. Defaults to "obj". + + + The runtime identifier to use. + + + Project '{Project}' targets framework '{targetFramework}'. The GetDocument Command-line Tool does not support this framework. + + + Using project '{project}'. + + + Show verbose output. + + + Writing '{file}'... + + + Project output not found and --no-build was specified. Project must be up-to-date when using the --no-build option. + + + The file to write the result to. + + + Unable to retrieve '{properrty}' project metadata. Ensure '{msbuildProperty}' is set. + + \ No newline at end of file diff --git a/src/dotnet-getdocument/ServiceProjectReferenceMetadata.props b/src/dotnet-getdocument/ServiceProjectReferenceMetadata.props new file mode 100644 index 0000000000..30f045f3c0 --- /dev/null +++ b/src/dotnet-getdocument/ServiceProjectReferenceMetadata.props @@ -0,0 +1,17 @@ + + + + + + $(WriteServiceProjectReferenceMetadataDependsOn) + + + + + $(WriteServiceProjectReferenceMetadataDependsOn) + + + $(WriteServiceProjectReferenceMetadataDependsOn) + + + diff --git a/src/dotnet-getdocument/ServiceProjectReferenceMetadata.targets b/src/dotnet-getdocument/ServiceProjectReferenceMetadata.targets new file mode 100644 index 0000000000..e8840ea37b --- /dev/null +++ b/src/dotnet-getdocument/ServiceProjectReferenceMetadata.targets @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dotnet-getdocument/dotnet-getdocument.csproj b/src/dotnet-getdocument/dotnet-getdocument.csproj new file mode 100644 index 0000000000..b88db8367a --- /dev/null +++ b/src/dotnet-getdocument/dotnet-getdocument.csproj @@ -0,0 +1,112 @@ + + + + + + + $(GenerateNuspecDependsOn);PopulateNuspec + + dotnet-getdocument + GetDocument Command-line Tool outside man + false + true + false + false + $(MSBuildProjectName).nuspec + Exe + true + GetDocument;command line;command-line;tool + GetDocument + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + + + + + TextTemplatingFileGenerator + Resources.Designer.cs + + + + + + + + + + True + True + Resources.Designer.tt + + + + + + + + + + + + + + + + <_Temporary Remove="@(Temporary)" /> + <_Temporary Include="../GetDocumentInsider/GetDocumentInsider.csproj" Properties="TargetFramework=net461" /> + <_Temporary Include="../GetDocumentInsider/GetDocumentInsider.csproj" Properties="TargetFramework=net461;Platform=x86" /> + <_Temporary Include="../GetDocumentInsider/GetDocumentInsider.csproj" Properties="TargetFramework=netcoreapp2.0" /> + + + + + + + <_Temporary Remove="@(_Temporary)" /> + + + + + authors=$(Authors); + copyright=$(Copyright); + description=$(Description); + iconUrl=$(PackageIconUrl); + id=$(PackageId); + InsiderNet461Output=..\GetDocumentInsider\bin\$(Configuration)\net461\publish\*; + InsiderNet461X86Output=..\GetDocumentInsider\bin\x86\$(Configuration)\net461\publish\*; + InsiderNetCoreOutput=..\GetDocumentInsider\bin\$(Configuration)\netcoreapp2.0\publish\*; + licenseUrl=$(PackageLicenseUrl); + Output=$(PublishDir)**\*; + OutputShims=$(IntermediateOutputPath)shims\**\*; + packageType=$(PackageType); + projectUrl=$(PackageProjectUrl); + repositoryCommit=$(RepositoryCommit); + repositoryUrl=$(RepositoryUrl); + SettingsFile=$(_ToolsSettingsFilePath); + tags=$(PackageTags.Replace(';', ' ')); + targetFramework=$(TargetFramework); + version=$(PackageVersion); + + + + diff --git a/src/dotnet-getdocument/dotnet-getdocument.nuspec b/src/dotnet-getdocument/dotnet-getdocument.nuspec new file mode 100644 index 0000000000..60c51f7869 --- /dev/null +++ b/src/dotnet-getdocument/dotnet-getdocument.nuspec @@ -0,0 +1,28 @@ + + + + $id$ + $version$ + $authors$ + true + $licenseUrl$ + $projectUrl$ + $iconUrl$ + $description$ + $copyright$ + $tags$ + + + + + + + + + + + + + + +