// 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; HttpMethods = Option("-m|--method", "allow multiple endpoints with different http method", CommandOptionType.NoValue); 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; } // Support multiple endpoints that are distinguished only by http method. public CommandOption HttpMethods { 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)); } Console.WriteLine($"Processing {Input.Values.Count} files..."); var entries = new List(); 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); } } // The data that we're provided by might be unambiguous. // Remove any routes that would be ambiguous in our system. var routesByPrecedence = new Dictionary>(); 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(); 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: {input}"); Out.WriteLine(ex); return new JObject(); } } } private void ParseEntries(JObject input, List entries) { var basePath = ""; if (input["basePath"] is JProperty basePathProperty) { basePath = basePathProperty.Value(); } 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 = HttpMethods.HasValue() ? method.Name.ToString() : null, 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 bool IsDuplicateTemplate(RouteEntry entry, List 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 (HttpMethods.HasValue() && !string.Equals(entry.Method, other.Method, StringComparison.OrdinalIgnoreCase)) { isSame = false; break; } } if (isSame) { return true; } } return false; } private static void Sort(List 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); } } }