using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using Webserver.UrlHandlers; namespace Webserver.WebAPI { public class OpenApiHelpers { private const string masterResourceName = "openapi.master.yaml"; private const string masterDocName = "openapi.yaml"; private struct OpenApiSpec { public readonly Dictionary ExportedPaths; public readonly string Spec; public OpenApiSpec (string _spec, Dictionary _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 == null || spec.ExportedPaths.Count < 1) { continue; } foreach ((string exportedPath, string rebasedPath) in spec.ExportedPaths) { writePath (sb, apiSpecName, exportedPath, rebasedPath); } } specs[masterDocName] = new OpenApiSpec(sb.ToString ()); Log.Out ("[Web] OpenAPI preparation done"); } private void writePath (StringBuilder _sb, string _apiSpecName, string _exportedPath, string _rebasedPath) { _sb.AppendLine ($" {_rebasedPath ?? _exportedPath}:"); _sb.Append ($" $ref: './{_apiSpecName}#/paths/"); writeJsonPointerEncodedPath (_sb, _exportedPath); _sb.AppendLine ("'"); } public void LoadOpenApiSpec (AbsWebAPI _api) { loadOpenApiSpec (_api.GetType ().Assembly, _api.Name, null); } public void LoadOpenApiSpec (AbsHandler _pathHandler) { Type type = _pathHandler.GetType (); loadOpenApiSpec (type.Assembly, type.Name, _pathHandler.UrlBasePath); } public void RegisterCustomSpec (Assembly _assembly, string _apiSpecName, string _replaceBasePath = null) { loadOpenApiSpec (_assembly, _apiSpecName, _replaceBasePath); } private void loadOpenApiSpec (Assembly _containingAssembly, string _apiName, string _basePath) { string apiSpecName = $"{_apiName}.openapi.yaml"; string specText = ResourceHelpers.GetManifestResourceText (_containingAssembly, apiSpecName, true); if (specText == null) { return; } // Log.Out ($"[Web] Loaded OpenAPI spec for '{_apiName}'"); OpenApiSpec spec = new OpenApiSpec (specText, findExportedPaths (specText, _basePath)); specs.Add (apiSpecName, spec); } private static readonly Regex pathMatcher = new Regex ("^\\s{1,2}(/\\S+):.*$", RegexOptions.Compiled | RegexOptions.CultureInvariant); private Dictionary findExportedPaths (string _spec, string _replaceBasePath = null) { Dictionary result = new Dictionary (); 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; string rebasedPath = null; // Log.Out ($"[Web] Exports: {path}"); if (_replaceBasePath != null) { rebasedPath = path.Replace ("/BASEPATH/", _replaceBasePath); } result [path] = rebasedPath; } } 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; } private void writeJsonPointerEncodedPath (StringBuilder _targetSb, string _path) { for (int i = 0; i < _path.Length; i++) { char c = _path[i]; switch (c) { // JSON string escaped characters case '"': _targetSb.Append ("\\\""); break; case '\\': _targetSb.Append ("\\\\"); break; case '\b': _targetSb.Append ("\\b"); break; case '\f': _targetSb.Append ("\\f"); break; case '\n': _targetSb.Append ("\\n"); break; case '\r': _targetSb.Append ("\\r"); break; case '\t': _targetSb.Append ("\\t"); break; case (char)0x00: _targetSb.Append ("\\u0000"); break; case (char)0x01: _targetSb.Append ("\\u0001"); break; case (char)0x02: _targetSb.Append ("\\u0002"); break; case (char)0x03: _targetSb.Append ("\\u0003"); break; case (char)0x04: _targetSb.Append ("\\u0004"); break; case (char)0x05: _targetSb.Append ("\\u0005"); break; case (char)0x06: _targetSb.Append ("\\u0006"); break; case (char)0x07: _targetSb.Append ("\\u0007"); break; case (char)0x0b: _targetSb.Append ("\\u000b"); break; case (char)0x0e: _targetSb.Append ("\\u000e"); break; case (char)0x0f: _targetSb.Append ("\\u000f"); break; case (char)0x10: _targetSb.Append ("\\u0010"); break; case (char)0x11: _targetSb.Append ("\\u0011"); break; case (char)0x12: _targetSb.Append ("\\u0012"); break; case (char)0x13: _targetSb.Append ("\\u0013"); break; case (char)0x14: _targetSb.Append ("\\u0014"); break; case (char)0x15: _targetSb.Append ("\\u0015"); break; case (char)0x16: _targetSb.Append ("\\u0016"); break; case (char)0x17: _targetSb.Append ("\\u0017"); break; case (char)0x18: _targetSb.Append ("\\u0018"); break; case (char)0x19: _targetSb.Append ("\\u0019"); break; case (char)0x1a: _targetSb.Append ("\\u001a"); break; case (char)0x1b: _targetSb.Append ("\\u001b"); break; case (char)0x1c: _targetSb.Append ("\\u001c"); break; case (char)0x1d: _targetSb.Append ("\\u001d"); break; case (char)0x1e: _targetSb.Append ("\\u001e"); break; case (char)0x1f: _targetSb.Append ("\\u001f"); break; // JSON Pointer specific case '/': _targetSb.Append ("~1"); break; case '~': _targetSb.Append ("~0"); break; // Non escaped characters default: _targetSb.Append (c); break; } } } } }