| 1 | using System; | 
|---|
| 2 | using System.Collections.Generic; | 
|---|
| 3 | using System.IO; | 
|---|
| 4 | using System.Reflection; | 
|---|
| 5 | using System.Text; | 
|---|
| 6 | using System.Text.RegularExpressions; | 
|---|
| 7 | using Webserver.UrlHandlers; | 
|---|
| 8 |  | 
|---|
| 9 | namespace Webserver.WebAPI { | 
|---|
| 10 | public class OpenApiHelpers { | 
|---|
| 11 | private const string masterResourceName = "openapi.master.yaml"; | 
|---|
| 12 | private const string masterDocName = "openapi.yaml"; | 
|---|
| 13 |  | 
|---|
| 14 | private struct OpenApiSpec { | 
|---|
| 15 | public readonly Dictionary<string, string> ExportedPaths; | 
|---|
| 16 | public readonly string Spec; | 
|---|
| 17 |  | 
|---|
| 18 | public OpenApiSpec (string _spec, Dictionary<string, string> _exportedPaths = null) { | 
|---|
| 19 | ExportedPaths = _exportedPaths; | 
|---|
| 20 | Spec = _spec; | 
|---|
| 21 | } | 
|---|
| 22 | } | 
|---|
| 23 |  | 
|---|
| 24 | private readonly Dictionary<string, OpenApiSpec> specs = new CaseInsensitiveStringDictionary<OpenApiSpec> (); | 
|---|
| 25 |  | 
|---|
| 26 | public OpenApiHelpers () { | 
|---|
| 27 | loadMainSpec (); | 
|---|
| 28 | Web.ServerInitialized += _ => { | 
|---|
| 29 | buildMainSpecRefs (); | 
|---|
| 30 | }; | 
|---|
| 31 | } | 
|---|
| 32 |  | 
|---|
| 33 | private void loadMainSpec () { | 
|---|
| 34 | Assembly apiAssembly = GetType ().Assembly; | 
|---|
| 35 |  | 
|---|
| 36 | string specText = ResourceHelpers.GetManifestResourceText (apiAssembly, masterResourceName, true); | 
|---|
| 37 | if (specText == null) { | 
|---|
| 38 | Log.Warning ($"[Web] Failed loading main OpenAPI spec from assembly '{Path.GetFileName (apiAssembly.Location)}'"); | 
|---|
| 39 | return; | 
|---|
| 40 | } | 
|---|
| 41 |  | 
|---|
| 42 | specs.Add (masterDocName, new OpenApiSpec(specText)); | 
|---|
| 43 | // Log.Out ($"[Web] Loaded main OpenAPI spec"); | 
|---|
| 44 | } | 
|---|
| 45 |  | 
|---|
| 46 | private void buildMainSpecRefs () { | 
|---|
| 47 | if (!TryGetOpenApiSpec (null, out string mainSpec)) { | 
|---|
| 48 | return; | 
|---|
| 49 | } | 
|---|
| 50 |  | 
|---|
| 51 | StringBuilder sb = new StringBuilder (mainSpec); | 
|---|
| 52 |  | 
|---|
| 53 | foreach ((string apiSpecName, OpenApiSpec spec) in specs) { | 
|---|
| 54 | if (apiSpecName.Equals (masterDocName)) { | 
|---|
| 55 | continue; | 
|---|
| 56 | } | 
|---|
| 57 |  | 
|---|
| 58 | if (spec.ExportedPaths == null || spec.ExportedPaths.Count < 1) { | 
|---|
| 59 | continue; | 
|---|
| 60 | } | 
|---|
| 61 |  | 
|---|
| 62 | foreach ((string exportedPath, string rebasedPath) in spec.ExportedPaths) { | 
|---|
| 63 | writePath (sb, apiSpecName, exportedPath, rebasedPath); | 
|---|
| 64 | } | 
|---|
| 65 | } | 
|---|
| 66 |  | 
|---|
| 67 | specs[masterDocName] = new OpenApiSpec(sb.ToString ()); | 
|---|
| 68 |  | 
|---|
| 69 | Log.Out ("[Web] OpenAPI preparation done"); | 
|---|
| 70 | } | 
|---|
| 71 |  | 
|---|
| 72 | private void writePath (StringBuilder _sb, string _apiSpecName, string _exportedPath, string _rebasedPath) { | 
|---|
| 73 | _sb.AppendLine ($"  {_rebasedPath ?? _exportedPath}:"); | 
|---|
| 74 | _sb.Append ($"    $ref: './{_apiSpecName}#/paths/"); | 
|---|
| 75 |  | 
|---|
| 76 | writeJsonPointerEncodedPath (_sb, _exportedPath); | 
|---|
| 77 |  | 
|---|
| 78 | _sb.AppendLine ("'"); | 
|---|
| 79 | } | 
|---|
| 80 |  | 
|---|
| 81 | public void LoadOpenApiSpec (AbsWebAPI _api) { | 
|---|
| 82 | loadOpenApiSpec (_api.GetType ().Assembly, _api.Name, null); | 
|---|
| 83 | } | 
|---|
| 84 |  | 
|---|
| 85 | public void LoadOpenApiSpec (AbsHandler _pathHandler) { | 
|---|
| 86 | Type type = _pathHandler.GetType (); | 
|---|
| 87 | loadOpenApiSpec (type.Assembly, type.Name, _pathHandler.UrlBasePath); | 
|---|
| 88 | } | 
|---|
| 89 |  | 
|---|
| 90 | public void RegisterCustomSpec (Assembly _assembly, string _apiSpecName, string _replaceBasePath = null) { | 
|---|
| 91 | loadOpenApiSpec (_assembly, _apiSpecName, _replaceBasePath); | 
|---|
| 92 | } | 
|---|
| 93 |  | 
|---|
| 94 | private void loadOpenApiSpec (Assembly _containingAssembly, string _apiName, string _basePath) { | 
|---|
| 95 | string apiSpecName = $"{_apiName}.openapi.yaml"; | 
|---|
| 96 |  | 
|---|
| 97 | string specText = ResourceHelpers.GetManifestResourceText (_containingAssembly, apiSpecName, true); | 
|---|
| 98 | if (specText == null) { | 
|---|
| 99 | return; | 
|---|
| 100 | } | 
|---|
| 101 |  | 
|---|
| 102 | // Log.Out ($"[Web] Loaded OpenAPI spec for '{_apiName}'"); | 
|---|
| 103 | OpenApiSpec spec = new OpenApiSpec (specText, findExportedPaths (specText, _basePath)); | 
|---|
| 104 | specs.Add (apiSpecName, spec); | 
|---|
| 105 | } | 
|---|
| 106 |  | 
|---|
| 107 | private static readonly Regex pathMatcher = new Regex ("^\\s{1,2}(/\\S+):.*$", RegexOptions.Compiled | RegexOptions.CultureInvariant); | 
|---|
| 108 | private Dictionary<string, string> findExportedPaths (string _spec, string _replaceBasePath = null) { | 
|---|
| 109 | Dictionary<string, string> result = new Dictionary<string, string> (); | 
|---|
| 110 |  | 
|---|
| 111 | using TextReader tr = new StringReader (_spec); | 
|---|
| 112 |  | 
|---|
| 113 | string line; | 
|---|
| 114 | bool inPaths = false; | 
|---|
| 115 | while ((line = tr.ReadLine ()) != null) { | 
|---|
| 116 | if (!inPaths) { | 
|---|
| 117 | if (line.StartsWith ("paths:")) { | 
|---|
| 118 | inPaths = true; | 
|---|
| 119 | } | 
|---|
| 120 | } else { | 
|---|
| 121 | Match match = pathMatcher.Match (line); | 
|---|
| 122 | if (!match.Success) { | 
|---|
| 123 | continue; | 
|---|
| 124 | } | 
|---|
| 125 |  | 
|---|
| 126 | string path = match.Groups [1].Value; | 
|---|
| 127 | string rebasedPath = null; | 
|---|
| 128 | // Log.Out ($"[Web]   Exports: {path}"); | 
|---|
| 129 | if (_replaceBasePath != null) { | 
|---|
| 130 | rebasedPath = path.Replace ("/BASEPATH/", _replaceBasePath); | 
|---|
| 131 | } | 
|---|
| 132 | result [path] = rebasedPath; | 
|---|
| 133 | } | 
|---|
| 134 | } | 
|---|
| 135 |  | 
|---|
| 136 | return result; | 
|---|
| 137 | } | 
|---|
| 138 |  | 
|---|
| 139 | public bool TryGetOpenApiSpec (string _name, out string _specText) { | 
|---|
| 140 | if (string.IsNullOrEmpty (_name)) { | 
|---|
| 141 | _name = masterDocName; | 
|---|
| 142 | } | 
|---|
| 143 |  | 
|---|
| 144 | if (!specs.TryGetValue (_name, out OpenApiSpec spec)) { | 
|---|
| 145 | _specText = null; | 
|---|
| 146 | return false; | 
|---|
| 147 | } | 
|---|
| 148 |  | 
|---|
| 149 | _specText = spec.Spec; | 
|---|
| 150 | return true; | 
|---|
| 151 | } | 
|---|
| 152 |  | 
|---|
| 153 | private void writeJsonPointerEncodedPath (StringBuilder _targetSb, string _path) { | 
|---|
| 154 | for (int i = 0; i < _path.Length; i++) { | 
|---|
| 155 | char c = _path[i]; | 
|---|
| 156 |  | 
|---|
| 157 | switch (c) { | 
|---|
| 158 | // JSON string escaped characters | 
|---|
| 159 | case '"': | 
|---|
| 160 | _targetSb.Append ("\\\""); | 
|---|
| 161 | break; | 
|---|
| 162 | case '\\': | 
|---|
| 163 | _targetSb.Append ("\\\\"); | 
|---|
| 164 | break; | 
|---|
| 165 | case '\b': | 
|---|
| 166 | _targetSb.Append ("\\b"); | 
|---|
| 167 | break; | 
|---|
| 168 | case '\f': | 
|---|
| 169 | _targetSb.Append ("\\f"); | 
|---|
| 170 | break; | 
|---|
| 171 | case '\n': | 
|---|
| 172 | _targetSb.Append ("\\n"); | 
|---|
| 173 | break; | 
|---|
| 174 | case '\r': | 
|---|
| 175 | _targetSb.Append ("\\r"); | 
|---|
| 176 | break; | 
|---|
| 177 | case '\t': | 
|---|
| 178 | _targetSb.Append ("\\t"); | 
|---|
| 179 | break; | 
|---|
| 180 | case (char)0x00: | 
|---|
| 181 | _targetSb.Append ("\\u0000"); | 
|---|
| 182 | break; | 
|---|
| 183 | case (char)0x01: | 
|---|
| 184 | _targetSb.Append ("\\u0001"); | 
|---|
| 185 | break; | 
|---|
| 186 | case (char)0x02: | 
|---|
| 187 | _targetSb.Append ("\\u0002"); | 
|---|
| 188 | break; | 
|---|
| 189 | case (char)0x03: | 
|---|
| 190 | _targetSb.Append ("\\u0003"); | 
|---|
| 191 | break; | 
|---|
| 192 | case (char)0x04: | 
|---|
| 193 | _targetSb.Append ("\\u0004"); | 
|---|
| 194 | break; | 
|---|
| 195 | case (char)0x05: | 
|---|
| 196 | _targetSb.Append ("\\u0005"); | 
|---|
| 197 | break; | 
|---|
| 198 | case (char)0x06: | 
|---|
| 199 | _targetSb.Append ("\\u0006"); | 
|---|
| 200 | break; | 
|---|
| 201 | case (char)0x07: | 
|---|
| 202 | _targetSb.Append ("\\u0007"); | 
|---|
| 203 | break; | 
|---|
| 204 | case (char)0x0b: | 
|---|
| 205 | _targetSb.Append ("\\u000b"); | 
|---|
| 206 | break; | 
|---|
| 207 | case (char)0x0e: | 
|---|
| 208 | _targetSb.Append ("\\u000e"); | 
|---|
| 209 | break; | 
|---|
| 210 | case (char)0x0f: | 
|---|
| 211 | _targetSb.Append ("\\u000f"); | 
|---|
| 212 | break; | 
|---|
| 213 | case (char)0x10: | 
|---|
| 214 | _targetSb.Append ("\\u0010"); | 
|---|
| 215 | break; | 
|---|
| 216 | case (char)0x11: | 
|---|
| 217 | _targetSb.Append ("\\u0011"); | 
|---|
| 218 | break; | 
|---|
| 219 | case (char)0x12: | 
|---|
| 220 | _targetSb.Append ("\\u0012"); | 
|---|
| 221 | break; | 
|---|
| 222 | case (char)0x13: | 
|---|
| 223 | _targetSb.Append ("\\u0013"); | 
|---|
| 224 | break; | 
|---|
| 225 | case (char)0x14: | 
|---|
| 226 | _targetSb.Append ("\\u0014"); | 
|---|
| 227 | break; | 
|---|
| 228 | case (char)0x15: | 
|---|
| 229 | _targetSb.Append ("\\u0015"); | 
|---|
| 230 | break; | 
|---|
| 231 | case (char)0x16: | 
|---|
| 232 | _targetSb.Append ("\\u0016"); | 
|---|
| 233 | break; | 
|---|
| 234 | case (char)0x17: | 
|---|
| 235 | _targetSb.Append ("\\u0017"); | 
|---|
| 236 | break; | 
|---|
| 237 | case (char)0x18: | 
|---|
| 238 | _targetSb.Append ("\\u0018"); | 
|---|
| 239 | break; | 
|---|
| 240 | case (char)0x19: | 
|---|
| 241 | _targetSb.Append ("\\u0019"); | 
|---|
| 242 | break; | 
|---|
| 243 | case (char)0x1a: | 
|---|
| 244 | _targetSb.Append ("\\u001a"); | 
|---|
| 245 | break; | 
|---|
| 246 | case (char)0x1b: | 
|---|
| 247 | _targetSb.Append ("\\u001b"); | 
|---|
| 248 | break; | 
|---|
| 249 | case (char)0x1c: | 
|---|
| 250 | _targetSb.Append ("\\u001c"); | 
|---|
| 251 | break; | 
|---|
| 252 | case (char)0x1d: | 
|---|
| 253 | _targetSb.Append ("\\u001d"); | 
|---|
| 254 | break; | 
|---|
| 255 | case (char)0x1e: | 
|---|
| 256 | _targetSb.Append ("\\u001e"); | 
|---|
| 257 | break; | 
|---|
| 258 | case (char)0x1f: | 
|---|
| 259 | _targetSb.Append ("\\u001f"); | 
|---|
| 260 | break; | 
|---|
| 261 | // JSON Pointer specific | 
|---|
| 262 | case '/': | 
|---|
| 263 | _targetSb.Append ("~1"); | 
|---|
| 264 | break; | 
|---|
| 265 | case '~': | 
|---|
| 266 | _targetSb.Append ("~0"); | 
|---|
| 267 | break; | 
|---|
| 268 | // Non escaped characters | 
|---|
| 269 | default: | 
|---|
| 270 | _targetSb.Append (c); | 
|---|
| 271 | break; | 
|---|
| 272 | } | 
|---|
| 273 | } | 
|---|
| 274 | } | 
|---|
| 275 |  | 
|---|
| 276 | } | 
|---|
| 277 | } | 
|---|