using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using System.Text.RegularExpressions; namespace Webserver.WebAPI { public class OpenApiHelpers { private const string masterResourceName = "openapi.master.yaml"; private const string masterDocName = "openapi.yaml"; private const string masterDocRefLineBegin = " - $ref: "; private const string masterDocRefLineEnd = "#/paths"; private struct OpenApiSpec { public readonly List ExportedPaths; public readonly string Spec; public OpenApiSpec (string _spec, List _exportedPaths = null) { ExportedPaths = _exportedPaths; Spec = _spec; } } private readonly Dictionary specs = new CaseInsensitiveStringDictionary (); public OpenApiHelpers () { loadMainSpec (); Web.ServerInitialized += _ => { buildMainSpecRefs (); }; } private void loadMainSpec () { Assembly apiAssembly = GetType ().Assembly; string specText = ResourceHelpers.GetManifestResourceText (apiAssembly, masterResourceName, true); if (specText == null) { Log.Warning ($"[Web] Failed loading main OpenAPI spec from assembly '{Path.GetFileName (apiAssembly.Location)}'"); return; } specs.Add (masterDocName, new OpenApiSpec(specText)); Log.Out ($"[Web] Loaded main OpenAPI spec"); } private void buildMainSpecRefs () { if (!TryGetOpenApiSpec (null, out string mainSpec)) { return; } StringBuilder sb = new StringBuilder (mainSpec); foreach ((string apiSpecName, OpenApiSpec spec) in specs) { if (apiSpecName.Equals (masterDocName)) { continue; } if ((spec.ExportedPaths?.Count ?? 0) < 1) { continue; } foreach (string exportedPath in spec.ExportedPaths) { writePath (sb, apiSpecName, exportedPath); } } specs[masterDocName] = new OpenApiSpec(sb.ToString ()); Log.Out ("[Web] OpenAPI preparation done"); } private void writePath (StringBuilder _sb, string _apiSpecName, string _exportedPath) { _sb.AppendLine ($" {_exportedPath}:"); _sb.Append ($" $ref: './{_apiSpecName}#/paths/"); for (int i = 0; i < _exportedPath.Length; i++) { char c = _exportedPath[i]; switch (c) { // JSON string escaped characters case '"': _sb.Append ("\\\""); break; case '\\': _sb.Append ("\\\\"); break; case '\b': _sb.Append ("\\b"); break; case '\f': _sb.Append ("\\f"); break; case '\n': _sb.Append ("\\n"); break; case '\r': _sb.Append ("\\r"); break; case '\t': _sb.Append ("\\t"); break; case (char)0x00: _sb.Append ("\\u0000"); break; case (char)0x01: _sb.Append ("\\u0001"); break; case (char)0x02: _sb.Append ("\\u0002"); break; case (char)0x03: _sb.Append ("\\u0003"); break; case (char)0x04: _sb.Append ("\\u0004"); break; case (char)0x05: _sb.Append ("\\u0005"); break; case (char)0x06: _sb.Append ("\\u0006"); break; case (char)0x07: _sb.Append ("\\u0007"); break; case (char)0x0b: _sb.Append ("\\u000b"); break; case (char)0x0e: _sb.Append ("\\u000e"); break; case (char)0x0f: _sb.Append ("\\u000f"); break; case (char)0x10: _sb.Append ("\\u0010"); break; case (char)0x11: _sb.Append ("\\u0011"); break; case (char)0x12: _sb.Append ("\\u0012"); break; case (char)0x13: _sb.Append ("\\u0013"); break; case (char)0x14: _sb.Append ("\\u0014"); break; case (char)0x15: _sb.Append ("\\u0015"); break; case (char)0x16: _sb.Append ("\\u0016"); break; case (char)0x17: _sb.Append ("\\u0017"); break; case (char)0x18: _sb.Append ("\\u0018"); break; case (char)0x19: _sb.Append ("\\u0019"); break; case (char)0x1a: _sb.Append ("\\u001a"); break; case (char)0x1b: _sb.Append ("\\u001b"); break; case (char)0x1c: _sb.Append ("\\u001c"); break; case (char)0x1d: _sb.Append ("\\u001d"); break; case (char)0x1e: _sb.Append ("\\u001e"); break; case (char)0x1f: _sb.Append ("\\u001f"); break; // JSON Pointer specific case '/': _sb.Append ("~1"); break; case '~': _sb.Append ("~0"); break; // Non escaped characters default: _sb.Append (c); break; } } _sb.AppendLine ("'"); } public void LoadOpenApiSpec (AbsWebAPI _api) { Assembly apiAssembly = _api.GetType ().Assembly; string apiName = _api.Name; string apiSpecName = $"{apiName}.openapi.yaml"; string specText = ResourceHelpers.GetManifestResourceText (apiAssembly, apiSpecName, true); if (specText == null) { return; } Log.Out ($"[Web] Loaded OpenAPI spec for '{apiName}'"); OpenApiSpec spec = new OpenApiSpec (specText, findExportedPaths (specText)); specs.Add (apiSpecName, spec); } private static readonly Regex pathMatcher = new Regex ("^\\s{1,2}(/\\S+):.*$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private List findExportedPaths (string _spec) { List result = new List (); using TextReader tr = new StringReader (_spec); string line; bool inPaths = false; while ((line = tr.ReadLine ()) != null) { if (!inPaths) { if (line.StartsWith ("paths:")) { inPaths = true; } } else { Match match = pathMatcher.Match (line); if (!match.Success) { continue; } string path = match.Groups [1].Value; Log.Out ($"[Web] Exports: {path}"); result.Add (path); } } return result; } public bool TryGetOpenApiSpec (string _name, out string _specText) { if (string.IsNullOrEmpty (_name)) { _name = masterDocName; } if (!specs.TryGetValue (_name, out OpenApiSpec spec)) { _specText = null; return false; } _specText = spec.Spec; return true; } } }