[466] | 1 | using System;
|
---|
[459] | 2 | using System.Collections.Generic;
|
---|
| 3 | using System.IO;
|
---|
| 4 | using System.Reflection;
|
---|
| 5 | using System.Text;
|
---|
[460] | 6 | using System.Text.RegularExpressions;
|
---|
[463] | 7 | using Webserver.UrlHandlers;
|
---|
[459] | 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 |
|
---|
[460] | 14 | private struct OpenApiSpec {
|
---|
[463] | 15 | public readonly Dictionary<string, string> ExportedPaths;
|
---|
[460] | 16 | public readonly string Spec;
|
---|
[459] | 17 |
|
---|
[463] | 18 | public OpenApiSpec (string _spec, Dictionary<string, string> _exportedPaths = null) {
|
---|
[460] | 19 | ExportedPaths = _exportedPaths;
|
---|
| 20 | Spec = _spec;
|
---|
| 21 | }
|
---|
| 22 | }
|
---|
| 23 |
|
---|
| 24 | private readonly Dictionary<string, OpenApiSpec> specs = new CaseInsensitiveStringDictionary<OpenApiSpec> ();
|
---|
| 25 |
|
---|
[459] | 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 |
|
---|
[460] | 42 | specs.Add (masterDocName, new OpenApiSpec(specText));
|
---|
[465] | 43 | // Log.Out ($"[Web] Loaded main OpenAPI spec");
|
---|
[459] | 44 | }
|
---|
| 45 |
|
---|
| 46 | private void buildMainSpecRefs () {
|
---|
| 47 | if (!TryGetOpenApiSpec (null, out string mainSpec)) {
|
---|
| 48 | return;
|
---|
| 49 | }
|
---|
| 50 |
|
---|
| 51 | StringBuilder sb = new StringBuilder (mainSpec);
|
---|
| 52 |
|
---|
[460] | 53 | foreach ((string apiSpecName, OpenApiSpec spec) in specs) {
|
---|
[459] | 54 | if (apiSpecName.Equals (masterDocName)) {
|
---|
| 55 | continue;
|
---|
| 56 | }
|
---|
| 57 |
|
---|
[463] | 58 | if (spec.ExportedPaths == null || spec.ExportedPaths.Count < 1) {
|
---|
[460] | 59 | continue;
|
---|
| 60 | }
|
---|
| 61 |
|
---|
[463] | 62 | foreach ((string exportedPath, string rebasedPath) in spec.ExportedPaths) {
|
---|
| 63 | writePath (sb, apiSpecName, exportedPath, rebasedPath);
|
---|
[460] | 64 | }
|
---|
[459] | 65 | }
|
---|
| 66 |
|
---|
[460] | 67 | specs[masterDocName] = new OpenApiSpec(sb.ToString ());
|
---|
[459] | 68 |
|
---|
| 69 | Log.Out ("[Web] OpenAPI preparation done");
|
---|
| 70 | }
|
---|
| 71 |
|
---|
[463] | 72 | private void writePath (StringBuilder _sb, string _apiSpecName, string _exportedPath, string _rebasedPath) {
|
---|
| 73 | _sb.AppendLine ($" {_rebasedPath ?? _exportedPath}:");
|
---|
[460] | 74 | _sb.Append ($" $ref: './{_apiSpecName}#/paths/");
|
---|
| 75 |
|
---|
[463] | 76 | writeJsonPointerEncodedPath (_sb, _exportedPath);
|
---|
| 77 |
|
---|
| 78 | _sb.AppendLine ("'");
|
---|
| 79 | }
|
---|
[460] | 80 |
|
---|
[463] | 81 | public void LoadOpenApiSpec (AbsWebAPI _api) {
|
---|
[466] | 82 | loadOpenApiSpec (_api.GetType ().Assembly, _api.Name, null);
|
---|
[463] | 83 | }
|
---|
| 84 |
|
---|
| 85 | public void LoadOpenApiSpec (AbsHandler _pathHandler) {
|
---|
[466] | 86 | Type type = _pathHandler.GetType ();
|
---|
| 87 | loadOpenApiSpec (type.Assembly, type.Name, _pathHandler.UrlBasePath);
|
---|
[463] | 88 | }
|
---|
| 89 |
|
---|
| 90 | public void RegisterCustomSpec (Assembly _assembly, string _apiSpecName, string _replaceBasePath = null) {
|
---|
[466] | 91 | loadOpenApiSpec (_assembly, _apiSpecName, _replaceBasePath);
|
---|
| 92 | }
|
---|
[463] | 93 |
|
---|
[466] | 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);
|
---|
[463] | 98 | if (specText == null) {
|
---|
| 99 | return;
|
---|
| 100 | }
|
---|
| 101 |
|
---|
[466] | 102 | // Log.Out ($"[Web] Loaded OpenAPI spec for '{_apiName}'");
|
---|
| 103 | OpenApiSpec spec = new OpenApiSpec (specText, findExportedPaths (specText, _basePath));
|
---|
[463] | 104 | specs.Add (apiSpecName, spec);
|
---|
| 105 | }
|
---|
| 106 |
|
---|
[487] | 107 | private static readonly Regex pathMatcher = new Regex (@"^\s{1,2}(/\S+):.*$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
---|
[463] | 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;
|
---|
[465] | 128 | // Log.Out ($"[Web] Exports: {path}");
|
---|
[463] | 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 |
|
---|
[460] | 157 | switch (c) {
|
---|
| 158 | // JSON string escaped characters
|
---|
| 159 | case '"':
|
---|
[463] | 160 | _targetSb.Append ("\\\"");
|
---|
[460] | 161 | break;
|
---|
| 162 | case '\\':
|
---|
[487] | 163 | _targetSb.Append (@"\\");
|
---|
[460] | 164 | break;
|
---|
| 165 | case '\b':
|
---|
[463] | 166 | _targetSb.Append ("\\b");
|
---|
[460] | 167 | break;
|
---|
| 168 | case '\f':
|
---|
[463] | 169 | _targetSb.Append ("\\f");
|
---|
[460] | 170 | break;
|
---|
| 171 | case '\n':
|
---|
[463] | 172 | _targetSb.Append ("\\n");
|
---|
[460] | 173 | break;
|
---|
| 174 | case '\r':
|
---|
[463] | 175 | _targetSb.Append ("\\r");
|
---|
[460] | 176 | break;
|
---|
| 177 | case '\t':
|
---|
[463] | 178 | _targetSb.Append ("\\t");
|
---|
[460] | 179 | break;
|
---|
| 180 | case (char)0x00:
|
---|
[463] | 181 | _targetSb.Append ("\\u0000");
|
---|
[460] | 182 | break;
|
---|
| 183 | case (char)0x01:
|
---|
[463] | 184 | _targetSb.Append ("\\u0001");
|
---|
[460] | 185 | break;
|
---|
| 186 | case (char)0x02:
|
---|
[463] | 187 | _targetSb.Append ("\\u0002");
|
---|
[460] | 188 | break;
|
---|
| 189 | case (char)0x03:
|
---|
[463] | 190 | _targetSb.Append ("\\u0003");
|
---|
[460] | 191 | break;
|
---|
| 192 | case (char)0x04:
|
---|
[463] | 193 | _targetSb.Append ("\\u0004");
|
---|
[460] | 194 | break;
|
---|
| 195 | case (char)0x05:
|
---|
[463] | 196 | _targetSb.Append ("\\u0005");
|
---|
[460] | 197 | break;
|
---|
| 198 | case (char)0x06:
|
---|
[463] | 199 | _targetSb.Append ("\\u0006");
|
---|
[460] | 200 | break;
|
---|
| 201 | case (char)0x07:
|
---|
[463] | 202 | _targetSb.Append ("\\u0007");
|
---|
[460] | 203 | break;
|
---|
| 204 | case (char)0x0b:
|
---|
[463] | 205 | _targetSb.Append ("\\u000b");
|
---|
[460] | 206 | break;
|
---|
| 207 | case (char)0x0e:
|
---|
[463] | 208 | _targetSb.Append ("\\u000e");
|
---|
[460] | 209 | break;
|
---|
| 210 | case (char)0x0f:
|
---|
[463] | 211 | _targetSb.Append ("\\u000f");
|
---|
[460] | 212 | break;
|
---|
| 213 | case (char)0x10:
|
---|
[463] | 214 | _targetSb.Append ("\\u0010");
|
---|
[460] | 215 | break;
|
---|
| 216 | case (char)0x11:
|
---|
[463] | 217 | _targetSb.Append ("\\u0011");
|
---|
[460] | 218 | break;
|
---|
| 219 | case (char)0x12:
|
---|
[463] | 220 | _targetSb.Append ("\\u0012");
|
---|
[460] | 221 | break;
|
---|
| 222 | case (char)0x13:
|
---|
[463] | 223 | _targetSb.Append ("\\u0013");
|
---|
[460] | 224 | break;
|
---|
| 225 | case (char)0x14:
|
---|
[463] | 226 | _targetSb.Append ("\\u0014");
|
---|
[460] | 227 | break;
|
---|
| 228 | case (char)0x15:
|
---|
[463] | 229 | _targetSb.Append ("\\u0015");
|
---|
[460] | 230 | break;
|
---|
| 231 | case (char)0x16:
|
---|
[463] | 232 | _targetSb.Append ("\\u0016");
|
---|
[460] | 233 | break;
|
---|
| 234 | case (char)0x17:
|
---|
[463] | 235 | _targetSb.Append ("\\u0017");
|
---|
[460] | 236 | break;
|
---|
| 237 | case (char)0x18:
|
---|
[463] | 238 | _targetSb.Append ("\\u0018");
|
---|
[460] | 239 | break;
|
---|
| 240 | case (char)0x19:
|
---|
[463] | 241 | _targetSb.Append ("\\u0019");
|
---|
[460] | 242 | break;
|
---|
| 243 | case (char)0x1a:
|
---|
[463] | 244 | _targetSb.Append ("\\u001a");
|
---|
[460] | 245 | break;
|
---|
| 246 | case (char)0x1b:
|
---|
[463] | 247 | _targetSb.Append ("\\u001b");
|
---|
[460] | 248 | break;
|
---|
| 249 | case (char)0x1c:
|
---|
[463] | 250 | _targetSb.Append ("\\u001c");
|
---|
[460] | 251 | break;
|
---|
| 252 | case (char)0x1d:
|
---|
[463] | 253 | _targetSb.Append ("\\u001d");
|
---|
[460] | 254 | break;
|
---|
| 255 | case (char)0x1e:
|
---|
[463] | 256 | _targetSb.Append ("\\u001e");
|
---|
[460] | 257 | break;
|
---|
| 258 | case (char)0x1f:
|
---|
[463] | 259 | _targetSb.Append ("\\u001f");
|
---|
[460] | 260 | break;
|
---|
| 261 | // JSON Pointer specific
|
---|
| 262 | case '/':
|
---|
[463] | 263 | _targetSb.Append ("~1");
|
---|
[460] | 264 | break;
|
---|
| 265 | case '~':
|
---|
[463] | 266 | _targetSb.Append ("~0");
|
---|
[460] | 267 | break;
|
---|
| 268 | // Non escaped characters
|
---|
| 269 | default:
|
---|
[463] | 270 | _targetSb.Append (c);
|
---|
[460] | 271 | break;
|
---|
| 272 | }
|
---|
| 273 | }
|
---|
| 274 | }
|
---|
| 275 |
|
---|
[459] | 276 | }
|
---|
| 277 | }
|
---|