rust_webx_openapi/
apiui.rs1pub const APIUI_HTML: &str = r##"<!DOCTYPE html>
9<html lang="en">
10<head>
11<meta charset="UTF-8">
12<meta name="viewport" content="width=device-width, initial-scale=1.0">
13<title>Rust WebX API Docs</title>
14<style>
15*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
16body{font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#c9d1d9;line-height:1.6}
17.container{max-width:960px;margin:0 auto;padding:2rem 1rem}
18.header{margin-bottom:2rem;padding-bottom:1.5rem;border-bottom:1px solid #21262d}
19.header h1{font-size:1.75rem;font-weight:600;color:#f0f6fc;margin-bottom:.25rem}
20.header .sub{font-size:.875rem;color:#8b949e}
21.tag-group{margin-bottom:2rem}
22.tag-header{display:flex;align-items:center;gap:.5rem;cursor:pointer;padding:.5rem 0;margin-bottom:.75rem;user-select:none}
23.tag-header:hover .tag-title{color:#79c0ff}
24.tag-arrow{color:#8b949e;font-size:.75rem;transition:transform .2s;width:16px;text-align:center}
25.tag-arrow.open{transform:rotate(90deg)}
26.tag-title{font-size:1.05rem;font-weight:600;color:#58a6ff;text-transform:capitalize}
27.tag-count{font-size:.75rem;color:#8b949e;background:#21262d;padding:1px 8px;border-radius:10px}
28.tag-body{display:none}
29.tag-body.open{display:block}
30.endpoint{margin-bottom:.5rem;border:1px solid #21262d;border-radius:8px;background:#161b22;overflow:hidden;transition:border-color .15s}
31.endpoint:hover{border-color:#30363d}
32.ep-summary{display:flex;align-items:center;padding:.625rem 1rem;cursor:pointer;gap:.75rem;user-select:none}
33.ep-summary:hover{background:#1c2129}
34.method{display:inline-block;min-width:60px;padding:2px 8px;border-radius:4px;font-size:.75rem;font-weight:700;text-align:center;text-transform:uppercase;flex-shrink:0}
35.method.get{background:#1b3a2d;color:#3fb950}
36.method.post{background:#1b3a4a;color:#58a6ff}
37.method.put{background:#3a2d1b;color:#d29922}
38.method.delete{background:#3a1b1b;color:#f85149}
39.method.patch{background:#2d1b3a;color:#bc8cff}
40.ep-path{font-family:'SFMono-Regular',Consolas,monospace;font-size:.9rem;color:#e6edf3;flex:1}
41.ep-summary-text{font-size:.8rem;color:#8b949e;margin-left:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
42.ep-detail{display:none;border-top:1px solid #21262d;padding:1rem}
43.ep-detail.open{display:block}
44.ep-section{margin-bottom:1.25rem}
45.ep-section:last-child{margin-bottom:0}
46.ep-section-title{font-size:.75rem;font-weight:600;color:#8b949e;text-transform:uppercase;margin-bottom:.5rem;letter-spacing:.5px}
47.param-table{width:100%;border-collapse:collapse;font-size:.85rem}
48.param-table th{text-align:left;padding:.35rem .5rem;color:#8b949e;font-weight:600;font-size:.75rem;text-transform:uppercase;border-bottom:1px solid #21262d}
49.param-table td{padding:.35rem .5rem;border-bottom:1px solid #1a1f27}
50.param-table tr:last-child td{border-bottom:none}
51.param-name{font-family:'SFMono-Regular',Consolas,monospace;color:#e6edf3}
52.param-in{color:#d2a8ff;font-size:.8rem}
53.param-type{font-family:'SFMono-Regular',Consolas,monospace;color:#7ee787;font-size:.82rem}
54.param-required{color:#f85149;font-weight:700;font-size:.75rem}
55.body-example{background:#0d1117;border:1px solid #21262d;border-radius:6px;padding:.75rem;overflow-x:auto}
56.body-example pre{font-family:'SFMono-Regular',Consolas,monospace;font-size:.8rem;color:#e6edf3;white-space:pre-wrap;word-break:break-all}
57.ep-description{font-size:.85rem;color:#8b949e;margin-bottom:.75rem;line-height:1.6;padding:.5rem .75rem;background:#0d1117;border-radius:6px;border:1px solid #21262d}
58.resp-list{list-style:none}
59.resp-item{display:flex;align-items:center;gap:.5rem;padding:.35rem 0;font-size:.85rem}
60.resp-status{display:inline-block;min-width:36px;padding:1px 6px;border-radius:4px;font-size:.75rem;font-weight:600;text-align:center}
61.resp-status.s2xx{background:#1b3a2d;color:#3fb950}
62.resp-status.s4xx{background:#3a2d1b;color:#d29922}
63.resp-status.s5xx{background:#3a1b1b;color:#f85149}
64.resp-desc{color:#c9d1d9}
65.copy-btn{display:inline-block;padding:2px 10px;font-size:.75rem;color:#58a6ff;background:#1b3a4a;border:1px solid #30363d;border-radius:4px;cursor:pointer;margin-top:.5rem;transition:background .15s}
66.copy-btn:hover{background:#1f4050}
67.copy-btn.copied{background:#1b3a2d;color:#3fb950;border-color:#3fb950}
68.try-btn{display:inline-block;padding:2px 10px;font-size:.75rem;color:#d29922;background:#3a2d1b;border:1px solid #5a4622;border-radius:4px;cursor:pointer;margin-top:.5rem;margin-left:.5rem;transition:background .15s}
69.try-btn:hover{background:#4a3d2b}
70.try-btn.active{background:#d29922;color:#0d1117;border-color:#d29922}
71.try-panel{display:none;margin-top:.75rem;padding:.75rem;background:#0a0e14;border:1px solid #21262d;border-radius:6px}
72.try-panel.open{display:block}
73.try-panel .path-builder{font-family:'SFMono-Regular',Consolas,monospace;font-size:.85rem;color:#e6edf3;margin-bottom:.75rem;padding:.5rem .75rem;background:#12171f;border-radius:4px;display:flex;flex-wrap:wrap;align-items:center;gap:2px}
74.try-panel .path-builder span{color:#8b949e}
75.try-panel .path-builder input{font-family:'SFMono-Regular',Consolas,monospace;font-size:.8rem;background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:3px;padding:2px 6px;min-width:80px;outline:none;transition:border-color .15s}
76.try-panel .path-builder input:focus{border-color:#58a6ff}
77.try-panel .body-input{margin-bottom:.75rem}
78.try-panel .body-input textarea{width:100%;min-width:100%;max-width:100%;min-height:80px;height:120px;resize:vertical;font-family:'SFMono-Regular',Consolas,monospace;font-size:.8rem;background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:4px;padding:.5rem .75rem;outline:none;transition:border-color .15s;line-height:1.5;tab-size:2}
79.try-panel .body-input textarea:focus{border-color:#58a6ff}
80.try-panel .body-input label{display:block;font-size:.7rem;font-weight:600;color:#8b949e;text-transform:uppercase;margin-bottom:.35rem;letter-spacing:.5px}
81.try-panel .send-btn{display:inline-block;padding:4px 16px;font-size:.75rem;font-weight:600;color:#fff;background:#238636;border:1px solid #2ea043;border-radius:4px;cursor:pointer;transition:background .15s}
82.try-panel .send-btn:hover{background:#2ea043}
83.try-panel .send-btn:disabled{opacity:.5;cursor:not-allowed}
84.resp-area{display:none;margin-top:.75rem;padding:.75rem;background:#0a0e14;border:1px solid #21262d;border-radius:6px}
85.resp-area.open{display:block}
86.resp-area .status{font-size:1.5rem;font-weight:700;margin-bottom:.5rem;padding-bottom:.5rem;border-bottom:1px solid #21262d}
87.resp-area .status.s2xx{color:#3fb950}
88.resp-area .status.s4xx{color:#d29922}
89.resp-area .status.s5xx{color:#f85149}
90.resp-area .body{font-family:'SFMono-Regular',Consolas,monospace;font-size:.8rem;color:#e6edf3;white-space:pre-wrap;word-break:break-all;background:#0d1117;padding:.5rem .75rem;border-radius:4px;max-height:400px;overflow-y:auto;line-height:1.5}
91.resp-area .loading-text{color:#8b949e;font-size:.85rem}
92.resp-area .error-text{color:#f85149;font-size:.85rem}
93.loading{text-align:center;padding:3rem 0;color:#8b949e}
94.error{text-align:center;padding:2rem;background:#3a1b1b;border-radius:6px;color:#f85149}
95.empty{text-align:center;padding:3rem 0;color:#8b949e}
96.badge{display:inline-block;padding:1px 6px;border-radius:4px;font-size:.7rem;font-weight:600;margin-left:.35rem}
97.badge.req{background:#3a2d1b;color:#d29922}
98</style>
99</head>
100<body>
101<div class="container">
102 <div class="header">
103 <h1 id="title">API Documentation</h1>
104 <div class="sub" id="version"></div>
105 </div>
106 <div id="content"><div class="loading">Loading API specification...</div></div>
107</div>
108<script>
109(function(){
110var content=document.getElementById('content');
111
112function h(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
113
114function buildBodyExample(params){
115 var obj={};
116 for(var i=0;i<params.length;i++){
117 var p=params[i];
118 if(p.source==='body'){obj.id='string';obj.name='string';obj.email='string';break;}
119 }
120 return JSON.stringify(obj,null,2);
121}
122
123function buildCurl(method,path,params){
124 var curl='curl -X '+method;
125 if(params){
126 var hasBody=false;
127 for(var i=0;i<params.length;i++){
128 if(params[i].source==='body'){hasBody=true;break;}
129 }
130 if(hasBody){
131 curl+=' -H "Content-Type: application/json" -d '+JSON.stringify(JSON.stringify({id:'string',name:'string',email:'string'}));
132 }
133 }
134 curl+=' "http://localhost:5000'+path+'"';
135 return curl;
136}
137
138function ucFirst(s){return s.charAt(0).toUpperCase()+s.slice(1);}
139
140function safeParams(params){return params||[];}
141
142function statusClass(code){
143 if(code>=200&&code<300)return's2xx';
144 if(code>=400&&code<500)return's4xx';
145 return's5xx';
146}
147
148function hasBodyMethod(method){
149 return method==='POST'||method==='PUT'||method==='PATCH';
150}
151
152function extractPathParams(path){
153 var re=/\{(\w+)\}/g;
154 var params=[];
155 var match;
156 while((match=re.exec(path))!==null){
157 params.push(match[1]);
158 }
159 return params;
160}
161
162function buildPathBuilderHTML(path){
163 var parts=[];
164 var last=0;
165 var re=/\{(\w+)\}/g;
166 var match;
167 while((match=re.exec(path))!==null){
168 if(match.index>last){
169 parts.push('<span>'+h(path.slice(last,match.index))+'</span>');
170 }
171 parts.push('<input type="text" class="path-param" data-param="'+h(match[1])+'" placeholder="'+h(match[1])+'" value="">');
172 last=re.lastIndex;
173 }
174 if(last<path.length){
175 parts.push('<span>'+h(path.slice(last))+'</span>');
176 }
177 if(parts.length===0){
178 parts.push('<span>'+h(path)+'</span>');
179 }
180 return parts.join('');
181}
182
183function buildTryPanelHTML(epId,method,path,params,requestBody){
184 var pathParams=extractPathParams(path);
185 var hasBody=hasBodyMethod(method);
186 var html='';
187 html+='<div class="try-panel" id="try-'+epId+'">';
188
189 if(pathParams.length>0){
190 html+='<div class="path-builder">'+buildPathBuilderHTML(path)+'</div>';
191 }
192
193 if(hasBody){
194 var bodyExample=requestBody||buildBodyExample(params);
195 html+='<div class="body-input">';
196 html+='<label>Request Body (JSON)</label>';
197 html+='<textarea class="try-body" id="req-body-'+epId+'">'+h(bodyExample)+'</textarea>';
198 html+='</div>';
199 }
200
201 html+='<button class="send-btn" onclick="executeRequest(\''+epId+'\',\''+h(method)+'\',\''+h(path)+'\')">Send</button>';
202 html+='<div class="resp-area" id="resp-'+epId+'">';
203 html+='<div class="status" id="status-'+epId+'"></div>';
204 html+='<div class="body" id="resp-body-'+epId+'"></div>';
205 html+='</div>';
206 html+='</div>';
207 return html;
208}
209
210function buildDescriptionHTML(description){
211 if(!description)return'';
212 return'<div class="ep-description">'+h(description).replace(/\n/g,'<br>')+'</div>';
213}
214
215fetch('/api/openapi.json')
216.then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.json();})
217.then(function(spec){
218 document.getElementById('title').textContent=spec.info.title||'API Documentation';
219 document.getElementById('version').textContent='v'+(spec.info.version||'0.0.0');
220
221 var paths=spec.paths||{};
222 var entries=Object.entries(paths);
223 if(entries.length===0){content.innerHTML='<div class="empty">No endpoints registered.</div>';return;}
224
225 var groups=new Map();
226 for(var i=0;i<entries.length;i++){
227 var path=entries[i][0];
228 var methods=entries[i][1];
229 var methodKeys=Object.keys(methods);
230 for(var j=0;j<methodKeys.length;j++){
231 var method=methodKeys[j];
232 var op=methods[method];
233 var tags=op.tags||['default'];
234 for(var k=0;k<tags.length;k++){
235 var tag=tags[k];
236 if(!groups.has(tag))groups.set(tag,[]);
237 groups.get(tag).push({
238 method:method.toUpperCase(),
239 path:path,
240 summary:op.summary||'',
241 description:op.description||'',
242 operationId:op.operationId||'',
243 parameters:safeParams(op.parameters),
244 requestBody:op.requestBody||null,
245 responses:op.responses||{},
246 });
247 }
248 }
249 }
250
251 var html='';
252 var tagKeys=Array.from(groups.keys());
253 for(var ti=0;ti<tagKeys.length;ti++){
254 var tag=tagKeys[ti];
255 var eps=groups.get(tag);
256 html+='<div class="tag-group">';
257 html+='<div class="tag-header" onclick="var b=this.nextElementSibling;var a=this.querySelector(\'.tag-arrow\');b.classList.toggle(\'open\');a.classList.toggle(\'open\')">';
258 html+='<span class="tag-arrow open">▶</span>';
259 html+='<span class="tag-title">'+h(tag)+'</span>';
260 html+='<span class="tag-count">'+eps.length+'</span>';
261 html+='</div>';
262 html+='<div class="tag-body open">';
263
264 for(var ei=0;ei<eps.length;ei++){
265 var ep=eps[ei];
266 var epId='ep-'+ti+'-'+ei;
267 html+='<div class="endpoint">';
268 html+='<div class="ep-summary" onclick="var d=document.getElementById(\''+epId+'\');d.classList.toggle(\'open\')">';
269 html+='<span class="method '+ep.method.toLowerCase()+'">'+ep.method+'</span>';
270 html+='<span class="ep-path">'+h(ep.path)+'</span>';
271 html+='<span class="ep-summary-text">'+h(ep.summary)+'</span>';
272 html+='</div>';
273 html+='<div class="ep-detail" id="'+epId+'">';
274
275 html+=buildDescriptionHTML(ep.description);
276
277 if(ep.parameters.length>0){
278 html+='<div class="ep-section"><div class="ep-section-title">Parameters</div>';
279 html+='<table class="param-table"><tr><th>Name</th><th>In</th><th>Type</th><th>Required</th></tr>';
280 for(var pi=0;pi<ep.parameters.length;pi++){
281 var p=ep.parameters[pi];
282 var ptype=p.schema?p.schema.type:'string';
283 html+='<tr><td class="param-name">'+h(p.name)+'</td>';
284 html+='<td class="param-in">'+h(p.in)+'</td>';
285 html+='<td class="param-type">'+h(ptype)+'</td>';
286 html+='<td class="param-required">'+(p.required?'required':'')+'</td></tr>';
287 }
288 html+='</table></div>';
289 }
290
291 if(ep.requestBody){
292 html+='<div class="ep-section"><div class="ep-section-title">Request Body <span class="badge req">application/json</span></div>';
293 html+='<div class="body-example"><pre>'+h(buildBodyExample(ep.parameters))+'</pre></div></div>';
294 }
295
296 var respKeys=Object.keys(ep.responses).sort();
297 if(respKeys.length>0){
298 html+='<div class="ep-section"><div class="ep-section-title">Responses</div><ul class="resp-list">';
299 for(var ri=0;ri<respKeys.length;ri++){
300 var code=respKeys[ri];
301 var resp=ep.responses[code];
302 html+='<li class="resp-item"><span class="resp-status '+statusClass(parseInt(code))+'">'+h(code)+'</span><span class="resp-desc">'+h(resp.description||'')+'</span></li>';
303 }
304 html+='</ul></div>';
305 }
306
307 html+='<button class="copy-btn" data-curl="'+h(buildCurl(ep.method,ep.path,ep.parameters))+'">Copy curl</button>';
308 html+='<button class="try-btn" data-ep="'+epId+'">Try it</button>';
309 html+=buildTryPanelHTML(epId,ep.method,ep.path,ep.parameters,ep.requestBody);
310 html+='</div></div>';
311 }
312
313 html+='</div></div>';
314 }
315
316 content.innerHTML=html;
317
318 // --- Event Delegation ---
319 content.addEventListener('click',function(e){
320 // Copy-curl button
321 var btn=e.target.closest('.copy-btn');
322 if(btn){
323 var curl=btn.getAttribute('data-curl');
324 if(!curl)return;
325 navigator.clipboard.writeText(curl).then(function(){
326 btn.textContent='Copied!';
327 btn.classList.add('copied');
328 setTimeout(function(){
329 btn.textContent='Copy curl';
330 btn.classList.remove('copied');
331 },2000);
332 }).catch(function(){
333 btn.textContent='Error';
334 });
335 return;
336 }
337
338 // Try-it button
339 var tryBtn=e.target.closest('.try-btn');
340 if(tryBtn){
341 var epId=tryBtn.getAttribute('data-ep');
342 var panel=document.getElementById('try-'+epId);
343 if(!panel)return;
344 var isOpen=panel.classList.contains('open');
345 if(isOpen){
346 panel.classList.remove('open');
347 tryBtn.classList.remove('active');
348 tryBtn.textContent='Try it';
349 }else{
350 panel.classList.add('open');
351 tryBtn.classList.add('active');
352 tryBtn.textContent='Hide';
353 }
354 return;
355 }
356 });
357})
358.catch(function(e){
359 content.innerHTML='<div class="error">Failed to load API spec: '+h(e.message)+'</div>';
360});
361})();
362
363// Global request execution function
364function executeRequest(epId,method,path){
365 var panel=document.getElementById('try-'+epId);
366 var respArea=document.getElementById('resp-'+epId);
367 var statusEl=document.getElementById('status-'+epId);
368 var bodyEl=document.getElementById('resp-body-'+epId);
369 var sendBtn=panel?panel.querySelector('.send-btn'):null;
370
371 respArea.classList.add('open');
372 statusEl.textContent='';
373 statusEl.className='status';
374 bodyEl.textContent='Sending...';
375 bodyEl.className='body';
376
377 if(sendBtn)sendBtn.disabled=true;
378
379 // Build URL by replacing path parameters
380 var url=path;
381 var pathInputs=panel?panel.querySelectorAll('.path-param'):[];
382 for(var i=0;i<pathInputs.length;i++){
383 var input=pathInputs[i];
384 var paramName=input.getAttribute('data-param');
385 var value=input.value.trim();
386 if(!value)value=paramName;
387 url=url.replace('{'+paramName+'}',encodeURIComponent(value));
388 }
389
390 var fetchOpts={method:method,headers:{}};
391
392 // Check for body
393 if(method==='POST'||method==='PUT'||method==='PATCH'){
394 var textarea=document.getElementById('req-body-'+epId);
395 if(textarea&&textarea.value.trim()){
396 fetchOpts.headers['Content-Type']='application/json';
397 fetchOpts.body=textarea.value;
398 }
399 }
400
401 fetch(url,fetchOpts)
402 .then(function(resp){
403 var sc=resp.status>=200&&resp.status<300?'s2xx':resp.status>=400&&resp.status<500?'s4xx':'s5xx';
404 statusEl.textContent=resp.status+' '+resp.statusText;
405 statusEl.className='status '+sc;
406 return resp.text().then(function(txt){return{status:resp.status,body:txt};});
407 })
408 .then(function(result){
409 var display=result.body;
410 try{
411 var parsed=JSON.parse(result.body);
412 display=JSON.stringify(parsed,null,2);
413 }catch(e){}
414 bodyEl.textContent=display;
415 bodyEl.className='body';
416 if(sendBtn)sendBtn.disabled=false;
417 })
418 .catch(function(err){
419 statusEl.textContent='Error';
420 statusEl.className='status s5xx';
421 bodyEl.textContent=err.message||'Request failed';
422 bodyEl.className='body error-text';
423 if(sendBtn)sendBtn.disabled=false;
424 });
425}
426</script>
427</body>
428</html>
429"##;