Add Swaggatherer (Swagger + Gatherer)

This is a new CLI tool that will generate a routing benchmark from an
input directory or file.

See following commits for examples
This commit is contained in:
Ryan Nowak 2018-06-05 21:45:20 -07:00
parent b27f032b43
commit aef5dc4e93
8 changed files with 423 additions and 14 deletions

View File

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27106.3000
MinimumVisualStudioVersion = 15.0.26730.03
@ -22,6 +22,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoutingSample.Web", "sample
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C430C499-382D-47BD-B351-CF8F89C08CD2}"
ProjectSection(SolutionItems) = preProject
build\dependencies.props = build\dependencies.props
global.json = global.json
NuGet.config = NuGet.config
EndProjectSection
@ -51,6 +52,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarkapps", "benchmarka
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarkapps\Benchmarks\Benchmarks.csproj", "{91F47A60-9A78-4968-B10D-157D9BFAC37F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Swaggatherer", "benchmarks\Swaggatherer\Swaggatherer.csproj", "{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -151,18 +154,6 @@ Global
{F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|x86.ActiveCfg = Release|Any CPU
{F3D86714-4E64-41A6-9B36-A47B3683CF5D}.Release|x86.Build.0 = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|x86.ActiveCfg = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|x86.Build.0 = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Any CPU.Build.0 = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.ActiveCfg = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.Build.0 = Release|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -175,6 +166,30 @@ Global
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|x86.ActiveCfg = Release|Any CPU
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5}.Release|x86.Build.0 = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|x86.ActiveCfg = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Debug|x86.Build.0 = Debug|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Any CPU.Build.0 = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.ActiveCfg = Release|Any CPU
{91F47A60-9A78-4968-B10D-157D9BFAC37F}.Release|x86.Build.0 = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|x86.ActiveCfg = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Debug|x86.Build.0 = Debug|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Any CPU.Build.0 = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|x86.ActiveCfg = Release|Any CPU
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -188,8 +203,9 @@ Global
{741B0B05-CE96-473B-B962-6B0A347DF79A} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
{5C73140B-41F3-466F-A07B-3614E4D80DF9} = {95359B4B-4C85-4B44-A75B-0621905C4CF6}
{F3D86714-4E64-41A6-9B36-A47B3683CF5D} = {D5F39F59-5725-4127-82E7-67028D006185}
{91F47A60-9A78-4968-B10D-157D9BFAC37F} = {7F5914E2-C63F-4759-898E-462804357C90}
{4EBE7A6F-3183-4A7D-B3D7-A6A9EC3867A5} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E}
{91F47A60-9A78-4968-B10D-157D9BFAC37F} = {7F5914E2-C63F-4759-898E-462804357C90}
{990ECDEE-49DE-45E3-B0D9-DDEB9CFF6A9D} = {D5F39F59-5725-4127-82E7-67028D006185}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {36C8D815-B7F1-479D-894B-E606FB8DECDA}

View File

@ -0,0 +1,14 @@
// 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 Swaggatherer
{
internal static class Program
{
public static void Main(string[] args)
{
var application = new SwaggathererApplication();
application.Execute(args);
}
}
}

View File

@ -0,0 +1,23 @@
# Swaggatherer (Swagger + Gatherer)
This is a cli tool that can generate a routing benchmark using a Swagger 2.0 spec as an input.
## Usage
Generate a benchmark from a swagger file:
```
dotnet run -- -i swagger.json -o MyGeneratedBenchark.generated.cs
```
Generate a benchmark from a directory of swagger files:
```
dotnet run -- -d /some/directory -o MyGeneratedBenchark.generated.cs
```
The directory mode will recursively search for `.json` files.
## Resources
A big repository of swagger docs: https://github.com/APIs-guru/openapi-directory
Swagger editor + yaml <-> json conversion tool: https://editor2.swagger.io
Azure's official swagger docs: https://github.com/Azure/azure-rest-api-specs

View File

@ -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 Microsoft.AspNetCore.Routing.Template;
namespace Swaggatherer
{
internal class RouteEntry
{
public RouteTemplate Template { get; set; }
public string Method { get; set; }
public decimal Precedence { get; set; }
public string RequestUrl { get; set; }
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Routing\Microsoft.AspNetCore.Routing.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,250 @@
// 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.Text;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.CommandLineUtils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Swaggatherer
{
internal class SwaggathererApplication : CommandLineApplication
{
public SwaggathererApplication()
{
Invoke = InvokeCore;
Input = Option("-i", "input swagger 2.0 JSON file", CommandOptionType.MultipleValue);
InputDirectory = Option("-d", "input directory", CommandOptionType.SingleValue);
Output = Option("-o", "output", CommandOptionType.SingleValue);
HelpOption("-h|--help");
}
public CommandOption Input { get; }
public CommandOption InputDirectory { get; }
public CommandOption Output { get; }
private int InvokeCore()
{
if (!Input.HasValue() && !InputDirectory.HasValue())
{
ShowHelp();
return 1;
}
if (Input.HasValue() && InputDirectory.HasValue())
{
ShowHelp();
return 1;
}
if (!Output.HasValue())
{
Output.Values.Add("Out.generated.cs");
}
if (InputDirectory.HasValue())
{
Input.Values.AddRange(Directory.EnumerateFiles(InputDirectory.Value(), "*.json", SearchOption.AllDirectories));
}
var entries = new List<RouteEntry>();
for (var i = 0; i < Input.Values.Count; i++)
{
var input = ReadInput(Input.Values[i]);
ParseEntries(input, entries);
}
// We don't yet want to support complex segments.
for (var i = entries.Count - 1; i >= 0; i--)
{
if (HasComplexSegment(entries[i]))
{
Out.WriteLine("Skipping route with complex segment: " + entries[i].Template.TemplateText);
entries.RemoveAt(i);
break;
}
}
// The data that we're provided by might be unambiguous.
// Remove any routes that would be ambiguous in our system.
var routesByPrecedence = new Dictionary<decimal, List<RouteEntry>>();
for (var i = entries.Count - 1; i >= 0; i--)
{
var entry = entries[i];
var precedence = RoutePrecedence.ComputeInbound(entries[i].Template);
if (!routesByPrecedence.TryGetValue(precedence, out var matches))
{
matches = new List<RouteEntry>();
routesByPrecedence.Add(precedence, matches);
}
if (IsDuplicateTemplate(entry, matches))
{
Out.WriteLine("Duplicate route template: " + entries[i].Template.TemplateText);
entries.RemoveAt(i);
continue;
}
matches.Add(entry);
}
// We're not too sophisticated with how we generate parameter values, just hoping for
// the best. For parameters we generate a segment that is the same length as the parameter name
// but with a minimum of 5 characters to avoid collisions.
for (var i = entries.Count - 1; i >= 0; i--)
{
entries[i].RequestUrl = GenerateRequestUrl(entries[i].Template);
if (entries[i].RequestUrl == null)
{
Out.WriteLine("Failed to create a request for: " + entries[i].Template.TemplateText);
entries.RemoveAt(i);
continue;
}
}
Sort(entries);
var text = Template.Execute(entries);
File.WriteAllText(Output.Value(), text);
return 0;
}
private JObject ReadInput(string input)
{
using (var reader = File.OpenText(input))
{
try
{
return JObject.Load(new JsonTextReader(reader));
}
catch (JsonReaderException ex)
{
Out.WriteLine("Error reading: {0}");
Out.WriteLine(ex);
return new JObject();
}
}
}
private static void ParseEntries(JObject input, List<RouteEntry> entries)
{
var basePath = "";
if (input["basePath"] is JProperty basePathProperty)
{
basePath = basePathProperty.Value<string>();
}
if (input["paths"] is JObject paths)
{
foreach (var path in paths.Properties())
{
foreach (var method in ((JObject)path.Value).Properties())
{
var template = basePath + path.Name;
var parsed = TemplateParser.Parse(template);
entries.Add(new RouteEntry()
{
Method = method.Name,
Template = parsed,
Precedence = RoutePrecedence.ComputeInbound(parsed),
});
}
}
}
}
private bool HasComplexSegment(RouteEntry entry)
{
for (var i = 0; i < entry.Template.Segments.Count; i++)
{
if (!entry.Template.Segments[i].IsSimple)
{
return true;
}
}
return false;
}
private static bool IsDuplicateTemplate(RouteEntry entry, List<RouteEntry> others)
{
for (var j = 0; j < others.Count; j++)
{
// This is another route with the same precedence. It is guaranteed to have the same number of segments
// of the same kinds and in the same order. We just need to check the literals.
var other = others[j];
var isSame = true;
for (var k = 0; k < entry.Template.Segments.Count; k++)
{
if (!string.Equals(
entry.Template.Segments[k].Parts[0].Text,
other.Template.Segments[k].Parts[0].Text,
StringComparison.OrdinalIgnoreCase))
{
isSame = false;
break;
}
}
if (isSame)
{
return true;
}
}
return false;
}
private static void Sort(List<RouteEntry> entries)
{
// We need to sort these in precedence order for the linear matchers.
entries.Sort((x, y) =>
{
var comparison = RoutePrecedence.ComputeInbound(x.Template).CompareTo(RoutePrecedence.ComputeInbound(y.Template));
if (comparison != 0)
{
return comparison;
}
return x.Template.TemplateText.CompareTo(y.Template.TemplateText);
});
}
private static string GenerateRequestUrl(RouteTemplate template)
{
if (template.Segments.Count == 0)
{
return "/";
}
var url = new StringBuilder();
for (var i = 0; i < template.Segments.Count; i++)
{
// We don't yet handle complex segments
var part = template.Segments[i].Parts[0];
url.Append("/");
url.Append(part.IsLiteral ? part.Text : GenerateParameterValue(part));
}
return url.ToString();
}
private static string GenerateParameterValue(TemplatePart part)
{
var text = Guid.NewGuid().ToString();
var length = Math.Min(text.Length, Math.Max(5, part.Name.Length));
return text.Substring(0, length);
}
}
}

View File

@ -0,0 +1,72 @@
// 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;
namespace Swaggatherer
{
internal static class Template
{
public static string Execute(IReadOnlyList<RouteEntry> entries)
{
var setupEndpointsLines = new List<string>();
for (var i = 0; i < entries.Count; i++)
{
setupEndpointsLines.Add($" _endpoints[{i}] = CreateEndpoint(\"{entries[i].Template.TemplateText}\");");
}
var setupRequestsLines = new List<string>();
for (var i = 0; i < entries.Count; i++)
{
setupRequestsLines.Add($" _requests[{i}] = new DefaultHttpContext();");
setupRequestsLines.Add($" _requests[{i}].RequestServices = CreateServices();");
setupRequestsLines.Add($" _requests[{i}].Request.Method = \"{entries[i].Method.ToUpperInvariant()}\";");
setupRequestsLines.Add($" _requests[{i}].Request.Path = \"{entries[i].RequestUrl}\";");
}
var setupMatcherLines = new List<string>();
for (var i = 0; i < entries.Count; i++)
{
setupMatcherLines.Add($" builder.AddEntry(\"{entries[i].Template.TemplateText}\", _endpoints[{i}]);");
}
return string.Format(@"
// 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.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing.Matchers
{{
// This code was generated by the Swaggatherer
public partial class GeneratedBenchmark : MatcherBenchmarkBase
{{
private const int EndpointCount = {3};
private void SetupEndpoints()
{{
_endpoints = new MatcherEndpoint[{3}];
{0}
}}
private void SetupRequests()
{{
_requests = new HttpContext[{3}];
{1}
}}
private Matcher SetupMatcher(MatcherBuilder builder)
{{
{2}
return builder.Build();
}}
}}
}}",
string.Join(Environment.NewLine, setupEndpointsLines),
string.Join(Environment.NewLine, setupRequestsLines),
string.Join(Environment.NewLine, setupMatcherLines),
entries.Count);
}
}
}

View File

@ -16,6 +16,7 @@
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.2.0-preview1-34373</MicrosoftAspNetCoreServerKestrelPackageVersion>
<MicrosoftAspNetCoreTestHostPackageVersion>2.2.0-preview1-34373</MicrosoftAspNetCoreTestHostPackageVersion>
<MicrosoftAspNetCoreTestingPackageVersion>2.2.0-preview1-34373</MicrosoftAspNetCoreTestingPackageVersion>
<MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>2.2.0-preview1-34373</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
<MicrosoftExtensionsConfigurationCommandLinePackageVersion>2.2.0-preview1-34373</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
<MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>2.2.0-preview1-34373</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
<MicrosoftExtensionsConfigurationPackageVersion>2.2.0-preview1-34373</MicrosoftExtensionsConfigurationPackageVersion>
@ -36,6 +37,7 @@
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
<MoqPackageVersion>4.7.49</MoqPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
<XunitAnalyzersPackageVersion>0.8.0</XunitAnalyzersPackageVersion>
<XunitPackageVersion>2.3.1</XunitPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>