source: TFP-WebServer/WebServer/src/WebAPI/OpenApiHelpers.cs@ 463

Last change on this file since 463 was 463, checked in by alloc, 15 months ago

21.1.16.0 release
Completed OpenAPI specs
Add support to path handlers to register OpenAPI specs
Fixed ItemIconHandler throwing error when requested path contains no dot

File size: 8.0 KB
Line 
1using System.Collections.Generic;
2using System.IO;
3using System.Reflection;
4using System.Text;
5using System.Text.RegularExpressions;
6using Webserver.UrlHandlers;
7
8namespace Webserver.WebAPI {
9 public class OpenApiHelpers {
10 private const string masterResourceName = "openapi.master.yaml";
11 private const string masterDocName = "openapi.yaml";
12
13 private struct OpenApiSpec {
14 public readonly Dictionary<string, string> ExportedPaths;
15 public readonly string Spec;
16
17 public OpenApiSpec (string _spec, Dictionary<string, string> _exportedPaths = null) {
18 ExportedPaths = _exportedPaths;
19 Spec = _spec;
20 }
21 }
22
23 private readonly Dictionary<string, OpenApiSpec> specs = new CaseInsensitiveStringDictionary<OpenApiSpec> ();
24
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
41 specs.Add (masterDocName, new OpenApiSpec(specText));
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
52 foreach ((string apiSpecName, OpenApiSpec spec) in specs) {
53 if (apiSpecName.Equals (masterDocName)) {
54 continue;
55 }
56
57 if (spec.ExportedPaths == null || spec.ExportedPaths.Count < 1) {
58 continue;
59 }
60
61 foreach ((string exportedPath, string rebasedPath) in spec.ExportedPaths) {
62 writePath (sb, apiSpecName, exportedPath, rebasedPath);
63 }
64 }
65
66 specs[masterDocName] = new OpenApiSpec(sb.ToString ());
67
68 Log.Out ("[Web] OpenAPI preparation done");
69 }
70
71 private void writePath (StringBuilder _sb, string _apiSpecName, string _exportedPath, string _rebasedPath) {
72 _sb.AppendLine ($" {_rebasedPath ?? _exportedPath}:");
73 _sb.Append ($" $ref: './{_apiSpecName}#/paths/");
74
75 writeJsonPointerEncodedPath (_sb, _exportedPath);
76
77 _sb.AppendLine ("'");
78 }
79
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
173 switch (c) {
174 // JSON string escaped characters
175 case '"':
176 _targetSb.Append ("\\\"");
177 break;
178 case '\\':
179 _targetSb.Append ("\\\\");
180 break;
181 case '\b':
182 _targetSb.Append ("\\b");
183 break;
184 case '\f':
185 _targetSb.Append ("\\f");
186 break;
187 case '\n':
188 _targetSb.Append ("\\n");
189 break;
190 case '\r':
191 _targetSb.Append ("\\r");
192 break;
193 case '\t':
194 _targetSb.Append ("\\t");
195 break;
196 case (char)0x00:
197 _targetSb.Append ("\\u0000");
198 break;
199 case (char)0x01:
200 _targetSb.Append ("\\u0001");
201 break;
202 case (char)0x02:
203 _targetSb.Append ("\\u0002");
204 break;
205 case (char)0x03:
206 _targetSb.Append ("\\u0003");
207 break;
208 case (char)0x04:
209 _targetSb.Append ("\\u0004");
210 break;
211 case (char)0x05:
212 _targetSb.Append ("\\u0005");
213 break;
214 case (char)0x06:
215 _targetSb.Append ("\\u0006");
216 break;
217 case (char)0x07:
218 _targetSb.Append ("\\u0007");
219 break;
220 case (char)0x0b:
221 _targetSb.Append ("\\u000b");
222 break;
223 case (char)0x0e:
224 _targetSb.Append ("\\u000e");
225 break;
226 case (char)0x0f:
227 _targetSb.Append ("\\u000f");
228 break;
229 case (char)0x10:
230 _targetSb.Append ("\\u0010");
231 break;
232 case (char)0x11:
233 _targetSb.Append ("\\u0011");
234 break;
235 case (char)0x12:
236 _targetSb.Append ("\\u0012");
237 break;
238 case (char)0x13:
239 _targetSb.Append ("\\u0013");
240 break;
241 case (char)0x14:
242 _targetSb.Append ("\\u0014");
243 break;
244 case (char)0x15:
245 _targetSb.Append ("\\u0015");
246 break;
247 case (char)0x16:
248 _targetSb.Append ("\\u0016");
249 break;
250 case (char)0x17:
251 _targetSb.Append ("\\u0017");
252 break;
253 case (char)0x18:
254 _targetSb.Append ("\\u0018");
255 break;
256 case (char)0x19:
257 _targetSb.Append ("\\u0019");
258 break;
259 case (char)0x1a:
260 _targetSb.Append ("\\u001a");
261 break;
262 case (char)0x1b:
263 _targetSb.Append ("\\u001b");
264 break;
265 case (char)0x1c:
266 _targetSb.Append ("\\u001c");
267 break;
268 case (char)0x1d:
269 _targetSb.Append ("\\u001d");
270 break;
271 case (char)0x1e:
272 _targetSb.Append ("\\u001e");
273 break;
274 case (char)0x1f:
275 _targetSb.Append ("\\u001f");
276 break;
277 // JSON Pointer specific
278 case '/':
279 _targetSb.Append ("~1");
280 break;
281 case '~':
282 _targetSb.Append ("~0");
283 break;
284 // Non escaped characters
285 default:
286 _targetSb.Append (c);
287 break;
288 }
289 }
290 }
291
292 }
293}
Note: See TracBrowser for help on using the repository browser.