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