Updated UI
This commit is contained in:
+1318
-256
File diff suppressed because it is too large
Load Diff
+455
-2
@@ -1610,7 +1610,7 @@
|
||||
dashboardPanel.style.display = "block";
|
||||
|
||||
try {
|
||||
const res = await fetch("analytics");
|
||||
const res = await fetch(new URL("analytics", document.baseURI));
|
||||
if (!res.ok) throw new Error("Failed to load analytics");
|
||||
|
||||
const analyticsData = await res.json();
|
||||
@@ -1619,7 +1619,7 @@
|
||||
// Refresh every 5 seconds
|
||||
if (window.dashboardInterval) clearInterval(window.dashboardInterval);
|
||||
window.dashboardInterval = setInterval(async () => {
|
||||
const res = await fetch("analytics");
|
||||
const res = await fetch(new URL("analytics", document.baseURI));
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
updateDashboard(data);
|
||||
@@ -1689,5 +1689,458 @@
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced Try Pane - Advanced Request Builder
|
||||
* Integrates RequestBuilder and CodeGenerator modules
|
||||
*/
|
||||
function renderAdvancedTryPane(group, endpoint) {
|
||||
if (!endpoint) return renderTryEmpty();
|
||||
|
||||
const paramState = new RequestBuilder.ParameterState();
|
||||
RequestBuilder.populateDefaults(endpoint, paramState);
|
||||
|
||||
let html = `
|
||||
<div class="try-wrap">
|
||||
<div class="try-header">
|
||||
<div class="try-title">${escapeHtml(endpoint.method)} ${escapeHtml(endpoint.path)}</div>
|
||||
${endpoint.deprecated ? '<span class="deprecated-badge">DEPRECATED</span>' : ''}
|
||||
</div>
|
||||
|
||||
<div class="params-editor">
|
||||
`;
|
||||
|
||||
// Path parameters
|
||||
if (endpoint.parameters?.filter(p => p.in === 'path').length > 0) {
|
||||
html += renderParamGroup('Path Parameters', endpoint.parameters.filter(p => p.in === 'path'), paramState);
|
||||
}
|
||||
|
||||
// Query parameters
|
||||
if (endpoint.parameters?.filter(p => p.in === 'query').length > 0) {
|
||||
html += renderParamGroup('Query Parameters', endpoint.parameters.filter(p => p.in === 'query'), paramState);
|
||||
}
|
||||
|
||||
// Headers
|
||||
if (endpoint.parameters?.filter(p => p.in === 'header').length > 0) {
|
||||
html += renderParamGroup('Headers', endpoint.parameters.filter(p => p.in === 'header'), paramState);
|
||||
}
|
||||
|
||||
// Request body
|
||||
if (endpoint.requestBody) {
|
||||
html += `
|
||||
<div class="param-group">
|
||||
<div class="param-group-header">
|
||||
<svg class="param-group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
Request Body
|
||||
</div>
|
||||
<div class="param-items">
|
||||
<div class="param-input" style="height: 200px; font-family: var(--font-mono); padding: 10px;" contenteditable="true" data-request-body="true">
|
||||
${endpoint.requestBody.example ? JSON.stringify(endpoint.requestBody.example, null, 2) : '{}'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
// Code Generator
|
||||
html += `
|
||||
<div class="code-generator">
|
||||
<div style="font-size: 12px; font-weight: 600; margin-bottom: 8px;">Generate Code</div>
|
||||
<div class="code-lang-select" id="codeLangSelect">
|
||||
`;
|
||||
|
||||
CodeGenerator.getSupportedLanguages().forEach(lang => {
|
||||
html += `<button class="code-lang-btn" data-lang="${lang}">${lang}</button>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<div class="code-block" id="codeBlock">
|
||||
// Select a language above
|
||||
</div>
|
||||
<button class="code-copy-btn" id="codeCopyBtn">Copy Code</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Response preview area
|
||||
html += `
|
||||
<div class="response-preview" style="display: none;" id="responsePreview">
|
||||
<div class="response-header">
|
||||
<span class="response-status" id="responseStatus"></span>
|
||||
<span class="response-timing" id="responseTiming"></span>
|
||||
</div>
|
||||
<div class="response-tabs">
|
||||
<button class="response-tab active" data-tab="body">Body</button>
|
||||
<button class="response-tab" data-tab="headers">Headers</button>
|
||||
</div>
|
||||
<div class="response-body" id="responseBody"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
el.try.innerHTML = html;
|
||||
|
||||
// Bind code generator
|
||||
document.querySelectorAll('.code-lang-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.code-lang-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
activeCodeLang = btn.dataset.lang;
|
||||
updateCodePreview(endpoint, paramState);
|
||||
});
|
||||
});
|
||||
|
||||
// Initial code preview
|
||||
updateCodePreview(endpoint, paramState);
|
||||
|
||||
// Copy code button
|
||||
document.getElementById('codeCopyBtn')?.addEventListener('click', () => {
|
||||
const code = document.getElementById('codeBlock').textContent;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const btn = document.getElementById('codeCopyBtn');
|
||||
btn.classList.add('copied');
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('copied');
|
||||
btn.textContent = 'Copy Code';
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render parameter group
|
||||
*/
|
||||
function renderParamGroup(name, parameters, paramState) {
|
||||
if (!parameters.length) return '';
|
||||
|
||||
const paramIn = parameters[0].in;
|
||||
let html = `
|
||||
<div class="param-group">
|
||||
<div class="param-group-header" data-toggle-group="${paramIn}">
|
||||
<svg class="param-group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
${name}
|
||||
</div>
|
||||
<div class="param-items">
|
||||
`;
|
||||
|
||||
parameters.forEach(param => {
|
||||
html += `
|
||||
<div class="param-row ${param.required ? 'required' : ''}">
|
||||
<div class="param-name" title="${escapeHtml(param.description || param.name)}">${escapeHtml(param.name)}</div>
|
||||
<input
|
||||
type="text"
|
||||
class="param-input"
|
||||
data-param-in="${param.in}"
|
||||
data-param-name="${param.name}"
|
||||
placeholder="${param.schema?.description || ''}"
|
||||
value="${paramState[param.in]?.[param.name] || ''}"
|
||||
/>
|
||||
<div class="param-type">${param.schema?.type || 'string'}</div>
|
||||
<button class="param-remove" title="Remove parameter">×</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `</div></div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update code preview based on current parameters
|
||||
*/
|
||||
function updateCodePreview(endpoint, paramState) {
|
||||
const currentLangBtn = document.querySelector('.code-lang-btn.active');
|
||||
const lang = currentLangBtn?.dataset.lang || 'curl';
|
||||
|
||||
// Gather current param values from UI
|
||||
document.querySelectorAll('.param-input').forEach(input => {
|
||||
const paramIn = input.dataset.paramIn;
|
||||
const paramName = input.dataset.paramName;
|
||||
if (paramIn && paramName) {
|
||||
paramState[paramIn][paramName] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Get request body if present
|
||||
const bodyInput = document.querySelector('[data-request-body="true"]');
|
||||
if (bodyInput) {
|
||||
try {
|
||||
paramState.body = JSON.parse(bodyInput.textContent);
|
||||
} catch (e) {
|
||||
paramState.body = bodyInput.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
const request = RequestBuilder.buildRequest(endpoint, paramState, window.location.origin);
|
||||
const code = CodeGenerator.generate(lang, request);
|
||||
document.getElementById('codeBlock').textContent = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration point for workspace manager
|
||||
*/
|
||||
function initializeWorkspaceManager() {
|
||||
// Load initial workspaces
|
||||
const workspaces = WorkspaceManager.getWorkspaces();
|
||||
if (workspaces.length === 0) {
|
||||
const defaultWs = new WorkspaceManager.Workspace('My Workspace');
|
||||
const defaultCol = new WorkspaceManager.Collection('API Collection');
|
||||
defaultWs.collections.push(defaultCol);
|
||||
WorkspaceManager.saveWorkspace(defaultWs);
|
||||
}
|
||||
|
||||
// Setup environment selector if environments panel exists
|
||||
const envList = document.querySelector('.env-list');
|
||||
if (envList) {
|
||||
const environments = WorkspaceManager.getEnvironments();
|
||||
if (environments.length === 0) {
|
||||
const defaultEnv = new WorkspaceManager.Environment('Default Environment');
|
||||
WorkspaceManager.saveEnvironment(defaultEnv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current request to collection
|
||||
*/
|
||||
function saveCurrentRequest() {
|
||||
if (!activeEndpoint || !spec) return;
|
||||
|
||||
const name = prompt('Request name:', `${activeEndpoint.method} ${activeEndpoint.path}`);
|
||||
if (!name) return;
|
||||
|
||||
const workspace = WorkspaceManager.getCurrentWorkspace();
|
||||
const collection = workspace.collections[0];
|
||||
if (!collection) return;
|
||||
|
||||
const saved = new WorkspaceManager.SavedRequest(
|
||||
name,
|
||||
activeEndpoint.method,
|
||||
activeEndpoint.path
|
||||
);
|
||||
saved.description = activeEndpoint.description || '';
|
||||
|
||||
// Gather current parameters
|
||||
document.querySelectorAll('.param-input').forEach(input => {
|
||||
const paramIn = input.dataset.paramIn;
|
||||
const paramName = input.dataset.paramName;
|
||||
if (paramIn && paramName && input.value) {
|
||||
saved.params[paramIn][paramName] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Save body
|
||||
const bodyInput = document.querySelector('[data-request-body="true"]');
|
||||
if (bodyInput) {
|
||||
saved.body = bodyInput.textContent;
|
||||
}
|
||||
|
||||
WorkspaceManager.saveRequestToCollection(workspace.id, collection.id, saved);
|
||||
alert(`Request saved to "${collection.name}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard shortcuts and accessibility
|
||||
*/
|
||||
function initializeKeyboardShortcuts() {
|
||||
const shortcuts = {
|
||||
'/': () => document.getElementById('searchInput')?.focus(),
|
||||
'KeyS': (e) => e.ctrlKey && saveCurrentRequest(),
|
||||
'KeyK': (e) => e.ctrlKey && toggleCommandPalette(),
|
||||
'Tab': (e) => handleTabNavigation(e),
|
||||
'Escape': () => closeModals()
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Don't trigger shortcuts when typing in input fields
|
||||
if (e.target.matches('input, textarea, [contenteditable]')) {
|
||||
if (e.key === 'Escape') {
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = shortcuts[e.code] || shortcuts[e.key];
|
||||
if (handler) {
|
||||
handler(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Show keyboard shortcut hints
|
||||
addShortcutHints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add visual keyboard shortcut hints to UI
|
||||
*/
|
||||
function addShortcutHints() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput && !searchInput.parentElement.querySelector('.kbd')) {
|
||||
const kbd = document.createElement('kbd');
|
||||
kbd.className = 'kbd search-kbd';
|
||||
kbd.textContent = '/';
|
||||
searchInput.parentElement.appendChild(kbd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle command palette (future enhancement)
|
||||
*/
|
||||
function toggleCommandPalette() {
|
||||
// Placeholder for future command palette implementation
|
||||
console.log('Command palette not yet implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Tab navigation for better accessibility
|
||||
*/
|
||||
function handleTabNavigation(e) {
|
||||
const focusableElements = document.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
if (focusableElements.length === 0) return;
|
||||
|
||||
const currentIndex = Array.from(focusableElements).indexOf(document.activeElement);
|
||||
let nextIndex = e.shiftKey ? currentIndex - 1 : currentIndex + 1;
|
||||
|
||||
if (nextIndex >= focusableElements.length) nextIndex = 0;
|
||||
if (nextIndex < 0) nextIndex = focusableElements.length - 1;
|
||||
|
||||
const nextElement = focusableElements[nextIndex];
|
||||
if (nextElement) {
|
||||
e.preventDefault();
|
||||
nextElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all open modals
|
||||
*/
|
||||
function closeModals() {
|
||||
document.querySelectorAll('[role="dialog"], .modal, .drawer').forEach(modal => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance accessibility with ARIA labels
|
||||
*/
|
||||
function enhanceAccessibility() {
|
||||
// Add ARIA roles and labels
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
if (sidebar) sidebar.setAttribute('role', 'navigation');
|
||||
|
||||
const detailPane = document.getElementById('detailPane');
|
||||
if (detailPane) detailPane.setAttribute('role', 'main');
|
||||
|
||||
const tryPane = document.getElementById('tryPane');
|
||||
if (tryPane) tryPane.setAttribute('role', 'complementary');
|
||||
|
||||
// Add aria-labels to icon buttons
|
||||
document.querySelectorAll('.icon-btn').forEach(btn => {
|
||||
if (!btn.getAttribute('aria-label')) {
|
||||
btn.setAttribute('aria-label', btn.title || btn.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
// Add roles to custom components
|
||||
document.querySelectorAll('.param-group-header').forEach(header => {
|
||||
header.setAttribute('role', 'button');
|
||||
header.setAttribute('tabindex', '0');
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-overview], [data-endpoint]').forEach(link => {
|
||||
link.setAttribute('role', 'menuitem');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add focus visible styles for keyboard navigation
|
||||
*/
|
||||
function initializeFocusStyles() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
input:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error handling and user feedback
|
||||
*/
|
||||
function showNotification(message, type = 'info', duration = 3000) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.setAttribute('role', 'status');
|
||||
notification.setAttribute('aria-live', 'polite');
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-1);
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideOut 0.3s ease-out';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved error logging
|
||||
*/
|
||||
const originalConsoleError = console.error;
|
||||
console.error = function(...args) {
|
||||
originalConsoleError.apply(console, args);
|
||||
const message = args[0]?.toString?.() || String(args[0]);
|
||||
if (message && !message.includes('HTTP')) {
|
||||
showNotification(`Error: ${message.substring(0, 50)}`, 'error', 5000);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize all enhancements
|
||||
*/
|
||||
function initializeEnhancements() {
|
||||
initializeWorkspaceManager();
|
||||
initializeKeyboardShortcuts();
|
||||
enhanceAccessibility();
|
||||
initializeFocusStyles();
|
||||
}
|
||||
|
||||
// Call on load
|
||||
initializeEnhancements();
|
||||
|
||||
init();
|
||||
})();
|
||||
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* Code Generation Module
|
||||
* Generates API request code snippets in multiple languages and frameworks
|
||||
*/
|
||||
const CodeGenerator = (() => {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Generate code in specified language
|
||||
*/
|
||||
function generate(language, request, options = {}) {
|
||||
const generator = generators[language.toLowerCase()];
|
||||
if (!generator) {
|
||||
return `// Language "${language}" not supported`;
|
||||
}
|
||||
return generator(request, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of supported languages
|
||||
*/
|
||||
function getSupportedLanguages() {
|
||||
return Object.keys(generators).sort();
|
||||
}
|
||||
|
||||
// Language generators
|
||||
const generators = {
|
||||
curl: (req) => generateCurl(req),
|
||||
fetch: (req) => generateFetch(req),
|
||||
axios: (req) => generateAxios(req),
|
||||
httpclient: (req) => generateHttpClient(req),
|
||||
httppy: (req) => generatePythonHttp(req),
|
||||
requests: (req) => generatePythonRequests(req),
|
||||
httpgo: (req) => generateGoHttp(req),
|
||||
csharp: (req) => generateCSharp(req),
|
||||
javascript: (req) => generateFetch(req),
|
||||
typescript: (req) => generateFetch(req),
|
||||
ruby: (req) => generateRuby(req),
|
||||
php: (req) => generatePhp(req),
|
||||
java: (req) => generateJava(req),
|
||||
kotlin: (req) => generateKotlin(req),
|
||||
powershell: (req) => generatePowerShell(req),
|
||||
swift: (req) => generateSwift(req),
|
||||
};
|
||||
|
||||
function escapeString(str) {
|
||||
return str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t');
|
||||
}
|
||||
|
||||
function escapeStringForJson(str) {
|
||||
return JSON.stringify(str);
|
||||
}
|
||||
|
||||
// ============= cURL =============
|
||||
function generateCurl(req) {
|
||||
let cmd = 'curl -X ' + req.method + ' ';
|
||||
|
||||
// Headers
|
||||
Object.entries(req.headers || {}).forEach(([key, value]) => {
|
||||
cmd += `-H ${escapeStringForJson(key + ': ' + value)} `;
|
||||
});
|
||||
|
||||
// Body
|
||||
if (req.body) {
|
||||
cmd += `-d ${escapeStringForJson(req.body)} `;
|
||||
}
|
||||
|
||||
// URL
|
||||
cmd += escapeStringForJson(req.url);
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// ============= Fetch API =============
|
||||
function generateFetch(req) {
|
||||
let code = `const url = '${escapeString(req.url)}';\n\n`;
|
||||
code += `const options = {\n`;
|
||||
code += ` method: '${req.method}',\n`;
|
||||
|
||||
if (Object.keys(req.headers || {}).length > 0) {
|
||||
code += ` headers: {\n`;
|
||||
Object.entries(req.headers).forEach(([key, value]) => {
|
||||
code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`;
|
||||
});
|
||||
code += ` },\n`;
|
||||
}
|
||||
|
||||
if (req.body) {
|
||||
code += ` body: ${typeof req.body === 'string' && req.body.startsWith('{') ? 'JSON.stringify(' + req.body + ')' : escapeStringForJson(req.body)},\n`;
|
||||
}
|
||||
|
||||
code += `};\n\n`;
|
||||
code += `try {\n`;
|
||||
code += ` const response = await fetch(url, options);\n`;
|
||||
code += ` const data = await response.json();\n`;
|
||||
code += ` console.log('Success:', data);\n`;
|
||||
code += `} catch (error) {\n`;
|
||||
code += ` console.error('Error:', error);\n`;
|
||||
code += `}`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= Axios =============
|
||||
function generateAxios(req) {
|
||||
let code = `import axios from 'axios';\n\n`;
|
||||
code += `const config = {\n`;
|
||||
code += ` method: '${req.method.toLowerCase()}',\n`;
|
||||
code += ` url: '${escapeString(req.url)}',\n`;
|
||||
|
||||
if (Object.keys(req.headers || {}).length > 0) {
|
||||
code += ` headers: {\n`;
|
||||
Object.entries(req.headers).forEach(([key, value]) => {
|
||||
code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`;
|
||||
});
|
||||
code += ` },\n`;
|
||||
}
|
||||
|
||||
if (req.body) {
|
||||
code += ` data: ${typeof req.body === 'string' && req.body.startsWith('{') ? req.body : escapeStringForJson(req.body)},\n`;
|
||||
}
|
||||
|
||||
code += `};\n\n`;
|
||||
code += `axios(config)\n`;
|
||||
code += ` .then((response) => console.log(response.data))\n`;
|
||||
code += ` .catch((error) => console.error(error));`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= Python - Requests =============
|
||||
function generatePythonRequests(req) {
|
||||
let code = `import requests\n\n`;
|
||||
code += `url = '${escapeString(req.url)}'\n`;
|
||||
|
||||
if (Object.keys(req.headers || {}).length > 0) {
|
||||
code += `headers = {\n`;
|
||||
Object.entries(req.headers).forEach(([key, value]) => {
|
||||
code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`;
|
||||
});
|
||||
code += `}\n`;
|
||||
} else {
|
||||
code += `headers = {}\n`;
|
||||
}
|
||||
|
||||
if (req.body) {
|
||||
code += `\ndata = ${req.body}\n\n`;
|
||||
code += `response = requests.${req.method.toLowerCase()}(url, json=data, headers=headers)\n`;
|
||||
} else {
|
||||
code += `\nresponse = requests.${req.method.toLowerCase()}(url, headers=headers)\n`;
|
||||
}
|
||||
|
||||
code += `print(response.json())`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= Python - HTTP =============
|
||||
function generatePythonHttp(req) {
|
||||
let code = `import http.client\nimport json\n\n`;
|
||||
const url = new URL(req.url);
|
||||
const host = url.hostname;
|
||||
const path = url.pathname + url.search;
|
||||
|
||||
code += `conn = http.client.HTTPSConnection("${host}")\n`;
|
||||
|
||||
if (Object.keys(req.headers || {}).length > 0) {
|
||||
code += `headers = {\n`;
|
||||
Object.entries(req.headers).forEach(([key, value]) => {
|
||||
code += ` "${escapeString(key)}": "${escapeString(String(value))}",\n`;
|
||||
});
|
||||
code += `}\n`;
|
||||
} else {
|
||||
code += `headers = {}\n`;
|
||||
}
|
||||
|
||||
if (req.body) {
|
||||
code += `\nbody = ${req.body}\n`;
|
||||
code += `conn.request("${req.method}", "${escapeString(path)}", json.dumps(body), headers)\n`;
|
||||
} else {
|
||||
code += `\nconn.request("${req.method}", "${escapeString(path)}", headers=headers)\n`;
|
||||
}
|
||||
|
||||
code += `response = conn.getresponse()\ndata = response.read()\nprint(data.decode())`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= Go =============
|
||||
function generateGoHttp(req) {
|
||||
let code = `package main\n\nimport (\n\t"fmt"\n\t"io"\n\t"net/http"\n)\n\n`;
|
||||
code += `func main() {\n`;
|
||||
code += `\treq, _ := http.NewRequest("${req.method}", "${escapeString(req.url)}", nil)\n`;
|
||||
|
||||
Object.entries(req.headers || {}).forEach(([key, value]) => {
|
||||
code += `\treq.Header.Add("${escapeString(key)}", "${escapeString(String(value))}")\n`;
|
||||
});
|
||||
|
||||
code += `\n\tres, _ := http.DefaultClient.Do(req)\n`;
|
||||
code += `\tdefer res.Body.Close()\n`;
|
||||
code += `\tbody, _ := io.ReadAll(res.Body)\n`;
|
||||
code += `\tfmt.Println(string(body))\n`;
|
||||
code += `}`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= C# / .NET =============
|
||||
function generateCSharp(req) {
|
||||
let code = `using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\n`;
|
||||
code += `class Program {\n`;
|
||||
code += ` static async Task Main() {\n`;
|
||||
code += ` using (var client = new HttpClient()) {\n`;
|
||||
|
||||
Object.entries(req.headers || {}).forEach(([key, value]) => {
|
||||
if (key.toLowerCase() !== 'content-type') {
|
||||
code += ` client.DefaultRequestHeaders.Add("${escapeString(key)}", "${escapeString(String(value))}");\n`;
|
||||
}
|
||||
});
|
||||
|
||||
if (req.body) {
|
||||
code += `\n var content = new StringContent("${escapeString(req.body)}", null, "application/json");\n`;
|
||||
code += ` var response = await client.${req.method.charAt(0) + req.method.slice(1).toLowerCase()}Async("${escapeString(req.url)}", content);\n`;
|
||||
} else {
|
||||
code += `\n var response = await client.GetAsync("${escapeString(req.url)}");\n`;
|
||||
}
|
||||
|
||||
code += ` var result = await response.Content.ReadAsStringAsync();\n`;
|
||||
code += ` Console.WriteLine(result);\n`;
|
||||
code += ` }\n`;
|
||||
code += ` }\n`;
|
||||
code += `}`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= Ruby =============
|
||||
function generateRuby(req) {
|
||||
let code = `require 'net/http'\nrequire 'json'\n\n`;
|
||||
code += `url = URI('${escapeString(req.url)}')\n`;
|
||||
code += `http = Net::HTTP.new(url.host, url.port)\n`;
|
||||
code += `http.use_ssl = true\n\n`;
|
||||
|
||||
const methodMap = { 'GET': 'Get', 'POST': 'Post', 'PUT': 'Put', 'PATCH': 'Patch', 'DELETE': 'Delete' };
|
||||
const method = methodMap[req.method] || 'Get';
|
||||
|
||||
code += `request = Net::HTTP::${method}.new(url.request_uri)\n`;
|
||||
|
||||
Object.entries(req.headers || {}).forEach(([key, value]) => {
|
||||
code += `request['${escapeString(key)}'] = '${escapeString(String(value))}'\n`;
|
||||
});
|
||||
|
||||
if (req.body) {
|
||||
code += `request.body = ${req.body}\n`;
|
||||
}
|
||||
|
||||
code += `\nresponse = http.request(request)\nputs response.body`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= PHP =============
|
||||
function generatePhp(req) {
|
||||
let code = `<?php\n\n$url = '${escapeString(req.url)}';\n`;
|
||||
code += `$method = '${req.method}';\n\n`;
|
||||
code += `$options = array(\n`;
|
||||
code += ` 'http' => array(\n`;
|
||||
code += ` 'method' => $method,\n`;
|
||||
|
||||
if (Object.keys(req.headers || {}).length > 0) {
|
||||
code += ` 'header' => array(\n`;
|
||||
Object.entries(req.headers).forEach(([key, value]) => {
|
||||
code += ` '${escapeString(key)}: ${String(value)}',\n`;
|
||||
});
|
||||
code += ` ),\n`;
|
||||
}
|
||||
|
||||
if (req.body) {
|
||||
code += ` 'content' => '${escapeString(req.body)}',\n`;
|
||||
}
|
||||
|
||||
code += ` )\n);\n\n`;
|
||||
code += `$context = stream_context_create($options);\n`;
|
||||
code += `$response = file_get_contents($url, false, $context);\n`;
|
||||
code += `echo $response;\n\n?>`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= Java =============
|
||||
function generateJava(req) {
|
||||
let code = `import java.net.http.HttpClient;\n`;
|
||||
code += `import java.net.http.HttpRequest;\n`;
|
||||
code += `import java.net.http.HttpResponse;\n`;
|
||||
code += `import java.net.URI;\n\n`;
|
||||
code += `public class ApiRequest {\n`;
|
||||
code += ` public static void main(String[] args) throws Exception {\n`;
|
||||
code += ` HttpClient client = HttpClient.newHttpClient();\n\n`;
|
||||
code += ` HttpRequest request = HttpRequest.newBuilder()\n`;
|
||||
code += ` .uri(URI.create("${escapeString(req.url)}"))\n`;
|
||||
code += ` .method("${req.method}", HttpRequest.BodyPublishers.noBody())\n`;
|
||||
|
||||
Object.entries(req.headers || {}).forEach(([key, value]) => {
|
||||
code += ` .header("${escapeString(key)}", "${escapeString(String(value))}")\n`;
|
||||
});
|
||||
|
||||
code += ` .build();\n\n`;
|
||||
code += ` HttpResponse<String> response = client.send(request,\n`;
|
||||
code += ` HttpResponse.BodyHandlers.ofString());\n\n`;
|
||||
code += ` System.out.println(response.body());\n`;
|
||||
code += ` }\n`;
|
||||
code += `}`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= Kotlin =============
|
||||
function generateKotlin(req) {
|
||||
let code = `import java.net.HttpURLConnection\nimport java.net.URL\n\n`;
|
||||
code += `fun main() {\n`;
|
||||
code += ` val url = URL("${escapeString(req.url)}")\n`;
|
||||
code += ` val connection = url.openConnection() as HttpURLConnection\n`;
|
||||
code += ` connection.requestMethod = "${req.method}"\n\n`;
|
||||
|
||||
Object.entries(req.headers || {}).forEach(([key, value]) => {
|
||||
code += ` connection.setRequestProperty("${escapeString(key)}", "${escapeString(String(value))}")\n`;
|
||||
});
|
||||
|
||||
code += `\n val inputStream = connection.inputStream\n`;
|
||||
code += ` val response = inputStream.bufferedReader().use { it.readText() }\n`;
|
||||
code += ` println(response)\n`;
|
||||
code += `}`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= PowerShell =============
|
||||
function generatePowerShell(req) {
|
||||
let code = `$uri = "${escapeString(req.url)}"\n`;
|
||||
code += `$method = "${req.method}"\n`;
|
||||
|
||||
if (Object.keys(req.headers || {}).length > 0) {
|
||||
code += `$headers = @{\n`;
|
||||
Object.entries(req.headers).forEach(([key, value]) => {
|
||||
code += ` "${escapeString(key)}" = "${escapeString(String(value))}"\n`;
|
||||
});
|
||||
code += `}\n\n`;
|
||||
}
|
||||
|
||||
if (req.body) {
|
||||
code += `$body = '${escapeString(req.body)}'\n`;
|
||||
code += `$response = Invoke-WebRequest -Uri $uri -Method $method -Headers $headers -Body $body\n`;
|
||||
} else {
|
||||
code += `$response = Invoke-WebRequest -Uri $uri -Method $method`;
|
||||
if (Object.keys(req.headers || {}).length > 0) {
|
||||
code += ` -Headers $headers`;
|
||||
}
|
||||
code += `\n`;
|
||||
}
|
||||
|
||||
code += `$response.Content`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// ============= Swift =============
|
||||
function generateSwift(req) {
|
||||
let code = `import Foundation\n\nlet url = URL(string: "${escapeString(req.url)}")!\n`;
|
||||
code += `var request = URLRequest(url: url)\n`;
|
||||
code += `request.httpMethod = "${req.method}"\n\n`;
|
||||
|
||||
Object.entries(req.headers || {}).forEach(([key, value]) => {
|
||||
code += `request.setValue("${escapeString(String(value))}", forHTTPHeaderField: "${escapeString(key)}")\n`;
|
||||
});
|
||||
|
||||
if (req.body) {
|
||||
code += `\nrequest.httpBody = "${escapeString(req.body)}".data(using: .utf8)\n`;
|
||||
}
|
||||
|
||||
code += `\nlet task = URLSession.shared.dataTask(with: request) { data, response, error in\n`;
|
||||
code += ` if let data = data {\n`;
|
||||
code += ` let json = String(data: data, encoding: .utf8)!\n`;
|
||||
code += ` print(json)\n`;
|
||||
code += ` }\n`;
|
||||
code += `}\n`;
|
||||
code += `task.resume()`;
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
generate,
|
||||
getSupportedLanguages,
|
||||
escapeString
|
||||
};
|
||||
})();
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = CodeGenerator;
|
||||
}
|
||||
@@ -115,6 +115,11 @@
|
||||
<input id="importFile" type="file" accept=".json" hidden />
|
||||
<input id="diffFile" type="file" accept=".json" hidden />
|
||||
|
||||
<!-- New request builder, code generator, and workspace manager modules -->
|
||||
<script src="request-builder.js"></script>
|
||||
<script src="code-generator.js"></script>
|
||||
<script src="workspace-manager.js"></script>
|
||||
|
||||
<script>window.__DOXA_API_SPEC_URL__ = "__DOXA_API_SPEC_PATH__";</script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* Advanced Request Builder Module
|
||||
* Handles parameter composition, validation, defaults, and intelligent request building
|
||||
*/
|
||||
const RequestBuilder = (() => {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Parameter state management
|
||||
*/
|
||||
class ParameterState {
|
||||
constructor() {
|
||||
this.path = {};
|
||||
this.query = {};
|
||||
this.header = {};
|
||||
this.cookie = {};
|
||||
this.body = null;
|
||||
this.bodyType = 'json'; // json, form, raw, binary
|
||||
this.formData = {};
|
||||
}
|
||||
|
||||
clone() {
|
||||
const copy = new ParameterState();
|
||||
copy.path = { ...this.path };
|
||||
copy.query = { ...this.query };
|
||||
copy.header = { ...this.header };
|
||||
copy.cookie = { ...this.cookie };
|
||||
copy.body = this.body;
|
||||
copy.bodyType = this.bodyType;
|
||||
copy.formData = { ...this.formData };
|
||||
return copy;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.path = {};
|
||||
this.query = {};
|
||||
this.header = {};
|
||||
this.cookie = {};
|
||||
this.body = null;
|
||||
this.bodyType = 'json';
|
||||
this.formData = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build request object from endpoint definition and parameter state
|
||||
*/
|
||||
function buildRequest(endpoint, paramState, baseUrl) {
|
||||
const req = {
|
||||
method: endpoint.method,
|
||||
path: resolvePath(endpoint.path, paramState.path),
|
||||
url: buildUrl(baseUrl, endpoint.path, paramState),
|
||||
headers: buildHeaders(endpoint, paramState),
|
||||
query: paramState.query,
|
||||
body: buildBody(endpoint, paramState),
|
||||
cookies: paramState.cookie
|
||||
};
|
||||
return req;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve path parameters
|
||||
*/
|
||||
function resolvePath(pathTemplate, pathParams) {
|
||||
let path = pathTemplate;
|
||||
Object.entries(pathParams).forEach(([key, value]) => {
|
||||
// Encode URI component but preserve slashes in path
|
||||
path = path.replace(`:${key}`, encodeURIComponent(String(value || '')));
|
||||
path = path.replace(`{${key}}`, encodeURIComponent(String(value || '')));
|
||||
});
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build complete URL with query parameters
|
||||
*/
|
||||
function buildUrl(baseUrl, path, paramState) {
|
||||
let resolvedPath = resolvePath(path, paramState.path);
|
||||
let url = baseUrl.replace(/\/$/, '') + resolvedPath;
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
Object.entries(paramState.query).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
queryParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
const qs = queryParams.toString();
|
||||
if (qs) url += '?' + qs;
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build headers including auth, content-type, etc
|
||||
*/
|
||||
function buildHeaders(endpoint, paramState) {
|
||||
const headers = {};
|
||||
|
||||
// User-provided headers
|
||||
Object.entries(paramState.header).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
headers[key] = String(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Auto content-type based on body
|
||||
if (paramState.body !== null && paramState.body !== undefined) {
|
||||
if (paramState.bodyType === 'json') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
} else if (paramState.bodyType === 'form') {
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
}
|
||||
|
||||
// Copy cookies to header if not already set
|
||||
if (Object.keys(paramState.cookie).length > 0) {
|
||||
const cookieStr = Object.entries(paramState.cookie)
|
||||
.filter(([, v]) => v != null && v !== '')
|
||||
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
||||
.join('; ');
|
||||
if (cookieStr) headers['Cookie'] = cookieStr;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build request body
|
||||
*/
|
||||
function buildBody(endpoint, paramState) {
|
||||
if (!paramState.body) return null;
|
||||
|
||||
if (paramState.bodyType === 'json') {
|
||||
if (typeof paramState.body === 'string') {
|
||||
try {
|
||||
// Validate JSON
|
||||
JSON.parse(paramState.body);
|
||||
return paramState.body;
|
||||
} catch (e) {
|
||||
return null; // Invalid JSON
|
||||
}
|
||||
}
|
||||
return JSON.stringify(paramState.body);
|
||||
}
|
||||
|
||||
if (paramState.bodyType === 'form') {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(paramState.formData).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
params.append(key, String(value));
|
||||
}
|
||||
});
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
return paramState.body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate parameters against endpoint schema
|
||||
*/
|
||||
function validateParameters(endpoint, paramState) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
if (!endpoint.parameters) return { errors, warnings };
|
||||
|
||||
endpoint.parameters.forEach((param) => {
|
||||
const value = paramState[param.in]?.[param.name];
|
||||
const isEmpty = value === undefined || value === null || value === '';
|
||||
|
||||
// Check required
|
||||
if (param.required && isEmpty) {
|
||||
errors.push(`Parameter '${param.name}' is required`);
|
||||
}
|
||||
|
||||
// Validate type and format
|
||||
if (!isEmpty) {
|
||||
const validation = validateParameterValue(value, param);
|
||||
if (!validation.valid) {
|
||||
errors.push(`Parameter '${param.name}': ${validation.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for deprecated params
|
||||
if (param.deprecated) {
|
||||
warnings.push(`Parameter '${param.name}' is deprecated`);
|
||||
}
|
||||
});
|
||||
|
||||
return { errors, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a parameter value against its schema
|
||||
*/
|
||||
function validateParameterValue(value, param) {
|
||||
const strValue = String(value);
|
||||
|
||||
// Validate type
|
||||
if (param.schema?.type) {
|
||||
switch (param.schema.type) {
|
||||
case 'integer':
|
||||
if (!/^-?\d+$/.test(strValue)) {
|
||||
return { valid: false, message: `must be an integer` };
|
||||
}
|
||||
if (param.schema.minimum !== undefined && parseInt(strValue) < param.schema.minimum) {
|
||||
return { valid: false, message: `must be >= ${param.schema.minimum}` };
|
||||
}
|
||||
if (param.schema.maximum !== undefined && parseInt(strValue) > param.schema.maximum) {
|
||||
return { valid: false, message: `must be <= ${param.schema.maximum}` };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
if (!/^-?\d+(\.\d+)?$/.test(strValue)) {
|
||||
return { valid: false, message: `must be a number` };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
if (!['true', 'false', '1', '0'].includes(strValue.toLowerCase())) {
|
||||
return { valid: false, message: `must be true or false` };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'string':
|
||||
if (param.schema.minLength !== undefined && strValue.length < param.schema.minLength) {
|
||||
return { valid: false, message: `must be at least ${param.schema.minLength} characters` };
|
||||
}
|
||||
if (param.schema.maxLength !== undefined && strValue.length > param.schema.maxLength) {
|
||||
return { valid: false, message: `must be at most ${param.schema.maxLength} characters` };
|
||||
}
|
||||
if (param.schema.pattern && !new RegExp(param.schema.pattern).test(strValue)) {
|
||||
return { valid: false, message: `invalid format` };
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate enum
|
||||
if (param.schema?.enum && !param.schema.enum.includes(value)) {
|
||||
return { valid: false, message: `must be one of: ${param.schema.enum.join(', ')}` };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill parameters from examples or defaults
|
||||
*/
|
||||
function populateDefaults(endpoint, paramState) {
|
||||
if (!endpoint.parameters) return;
|
||||
|
||||
endpoint.parameters.forEach((param) => {
|
||||
if (!paramState[param.in]) paramState[param.in] = {};
|
||||
|
||||
// Skip if already set
|
||||
if (paramState[param.in][param.name] !== undefined && paramState[param.in][param.name] !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try example
|
||||
if (param.example !== undefined) {
|
||||
paramState[param.in][param.name] = param.example;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try schema default
|
||||
if (param.schema?.default !== undefined) {
|
||||
paramState[param.in][param.name] = param.schema.default;
|
||||
return;
|
||||
}
|
||||
|
||||
// Provide smart defaults based on type
|
||||
if (param.schema?.type) {
|
||||
switch (param.schema.type) {
|
||||
case 'integer':
|
||||
paramState[param.in][param.name] = param.schema.minimum || 1;
|
||||
break;
|
||||
case 'number':
|
||||
paramState[param.in][param.name] = param.schema.minimum || 0.0;
|
||||
break;
|
||||
case 'boolean':
|
||||
paramState[param.in][param.name] = true;
|
||||
break;
|
||||
case 'string':
|
||||
paramState[param.in][param.name] = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate example body from schema
|
||||
*/
|
||||
function generateExampleBody(schema, schemas) {
|
||||
if (!schema) return null;
|
||||
|
||||
// Handle references
|
||||
if (schema.$ref) {
|
||||
const refName = schema.$ref.split('/').pop();
|
||||
schema = schemas?.[refName] || schema;
|
||||
}
|
||||
|
||||
if (schema.type === 'object') {
|
||||
const example = {};
|
||||
if (schema.properties) {
|
||||
Object.entries(schema.properties).forEach(([key, propSchema]) => {
|
||||
if (propSchema.example !== undefined) {
|
||||
example[key] = propSchema.example;
|
||||
} else if (propSchema.type === 'string') {
|
||||
example[key] = `<${key}>`;
|
||||
} else if (propSchema.type === 'integer') {
|
||||
example[key] = propSchema.minimum || 1;
|
||||
} else if (propSchema.type === 'number') {
|
||||
example[key] = propSchema.minimum || 0.0;
|
||||
} else if (propSchema.type === 'boolean') {
|
||||
example[key] = true;
|
||||
} else if (propSchema.type === 'array') {
|
||||
example[key] = [generateExampleBody(propSchema.items, schemas)];
|
||||
}
|
||||
});
|
||||
}
|
||||
return example;
|
||||
}
|
||||
|
||||
if (schema.type === 'array') {
|
||||
return [generateExampleBody(schema.items, schemas)];
|
||||
}
|
||||
|
||||
if (schema.example !== undefined) return schema.example;
|
||||
|
||||
switch (schema.type) {
|
||||
case 'string': return 'example';
|
||||
case 'integer': return 0;
|
||||
case 'number': return 0.0;
|
||||
case 'boolean': return true;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract suggestion data from response to prefill next request
|
||||
*/
|
||||
function extractSuggestions(response, responseSchema) {
|
||||
const suggestions = {};
|
||||
|
||||
if (!response || typeof response !== 'object') return suggestions;
|
||||
|
||||
if (responseSchema?.type === 'object' && responseSchema.properties) {
|
||||
Object.keys(responseSchema.properties).forEach((key) => {
|
||||
if (response[key] !== undefined) {
|
||||
suggestions[key] = response[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is ready to send
|
||||
*/
|
||||
function isRequestReady(endpoint, paramState) {
|
||||
if (!endpoint) return false;
|
||||
if (!endpoint.parameters) return true;
|
||||
|
||||
const hasErrors = endpoint.parameters.some((param) => {
|
||||
if (!param.required) return false;
|
||||
const value = paramState[param.in]?.[param.name];
|
||||
return value === undefined || value === null || value === '';
|
||||
});
|
||||
|
||||
return !hasErrors;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
ParameterState,
|
||||
buildRequest,
|
||||
resolvePath,
|
||||
buildUrl,
|
||||
buildHeaders,
|
||||
buildBody,
|
||||
validateParameters,
|
||||
validateParameterValue,
|
||||
populateDefaults,
|
||||
generateExampleBody,
|
||||
extractSuggestions,
|
||||
isRequestReady
|
||||
};
|
||||
})();
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = RequestBuilder;
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
/**
|
||||
* Workspace Manager Module
|
||||
* Handles request collections, workspaces, environments, and persistence
|
||||
*/
|
||||
const WorkspaceManager = (() => {
|
||||
"use strict";
|
||||
|
||||
const STORAGE_KEY = 'DoxaApi.workspaces';
|
||||
const CURRENT_WS_KEY = 'DoxaApi.currentWorkspace';
|
||||
const ENV_KEY = 'DoxaApi.environments';
|
||||
|
||||
/**
|
||||
* Workspace structure
|
||||
*/
|
||||
class Workspace {
|
||||
constructor(name = 'Default') {
|
||||
this.id = generateId();
|
||||
this.name = name;
|
||||
this.description = '';
|
||||
this.collections = [];
|
||||
this.createdAt = Date.now();
|
||||
this.updatedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection structure (folders for organizing requests)
|
||||
*/
|
||||
class Collection {
|
||||
constructor(name = 'New Collection') {
|
||||
this.id = generateId();
|
||||
this.name = name;
|
||||
this.description = '';
|
||||
this.requests = [];
|
||||
this.folders = [];
|
||||
this.createdAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request in a collection
|
||||
*/
|
||||
class SavedRequest {
|
||||
constructor(name = 'New Request', method = 'GET', path = '') {
|
||||
this.id = generateId();
|
||||
this.name = name;
|
||||
this.method = method;
|
||||
this.path = path;
|
||||
this.description = '';
|
||||
this.tags = [];
|
||||
this.params = {
|
||||
path: {},
|
||||
query: {},
|
||||
header: {},
|
||||
cookie: {}
|
||||
};
|
||||
this.body = null;
|
||||
this.bodyType = 'json';
|
||||
this.formData = {};
|
||||
this.auth = null;
|
||||
this.createdAt = Date.now();
|
||||
this.updatedAt = Date.now();
|
||||
}
|
||||
|
||||
clone() {
|
||||
const copy = new SavedRequest(this.name, this.method, this.path);
|
||||
copy.id = generateId();
|
||||
copy.description = this.description;
|
||||
copy.tags = [...this.tags];
|
||||
copy.params = JSON.parse(JSON.stringify(this.params));
|
||||
copy.body = this.body;
|
||||
copy.bodyType = this.bodyType;
|
||||
copy.formData = { ...this.formData };
|
||||
copy.auth = this.auth;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment for variable substitution
|
||||
*/
|
||||
class Environment {
|
||||
constructor(name = 'Default') {
|
||||
this.id = generateId();
|
||||
this.name = name;
|
||||
this.description = '';
|
||||
this.variables = {};
|
||||
this.isActive = false;
|
||||
this.createdAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique ID
|
||||
*/
|
||||
function generateId() {
|
||||
return 'id_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all workspaces
|
||||
*/
|
||||
function getWorkspaces() {
|
||||
try {
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
return data ? JSON.parse(data).map(deserializeWorkspace) : [];
|
||||
} catch (e) {
|
||||
console.error('Failed to load workspaces:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current active workspace
|
||||
*/
|
||||
function getCurrentWorkspace() {
|
||||
const workspaces = getWorkspaces();
|
||||
if (workspaces.length === 0) {
|
||||
const ws = new Workspace('Default');
|
||||
saveWorkspace(ws);
|
||||
return ws;
|
||||
}
|
||||
return workspaces[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save workspace
|
||||
*/
|
||||
function saveWorkspace(workspace) {
|
||||
workspace.updatedAt = Date.now();
|
||||
const workspaces = getWorkspaces();
|
||||
const index = workspaces.findIndex(w => w.id === workspace.id);
|
||||
|
||||
if (index >= 0) {
|
||||
workspaces[index] = workspace;
|
||||
} else {
|
||||
workspaces.push(workspace);
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(workspaces));
|
||||
return workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete workspace
|
||||
*/
|
||||
function deleteWorkspace(workspaceId) {
|
||||
let workspaces = getWorkspaces();
|
||||
workspaces = workspaces.filter(w => w.id !== workspaceId);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(workspaces));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add collection to workspace
|
||||
*/
|
||||
function addCollection(workspaceId, collection) {
|
||||
const workspace = getWorkspaces().find(w => w.id === workspaceId);
|
||||
if (workspace) {
|
||||
workspace.collections.push(collection);
|
||||
saveWorkspace(workspace);
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save request to collection
|
||||
*/
|
||||
function saveRequestToCollection(workspaceId, collectionId, request) {
|
||||
const workspace = getWorkspaces().find(w => w.id === workspaceId);
|
||||
if (!workspace) return null;
|
||||
|
||||
const collection = findCollectionRecursive(workspace.collections, collectionId);
|
||||
if (!collection) return null;
|
||||
|
||||
request.updatedAt = Date.now();
|
||||
const index = collection.requests.findIndex(r => r.id === request.id);
|
||||
|
||||
if (index >= 0) {
|
||||
collection.requests[index] = request;
|
||||
} else {
|
||||
collection.requests.push(request);
|
||||
}
|
||||
|
||||
saveWorkspace(workspace);
|
||||
return request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find collection recursively in folder structure
|
||||
*/
|
||||
function findCollectionRecursive(collections, collectionId) {
|
||||
for (const col of collections) {
|
||||
if (col.id === collectionId) return col;
|
||||
if (col.folders) {
|
||||
const found = findCollectionRecursive(col.folders, collectionId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all requests in workspace
|
||||
*/
|
||||
function getAllRequests(workspaceId) {
|
||||
const workspace = getWorkspaces().find(w => w.id === workspaceId);
|
||||
if (!workspace) return [];
|
||||
|
||||
const requests = [];
|
||||
|
||||
function traverse(collections) {
|
||||
collections.forEach(col => {
|
||||
requests.push(...(col.requests || []));
|
||||
if (col.folders) {
|
||||
traverse(col.folders);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
traverse(workspace.collections);
|
||||
return requests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search requests in workspace
|
||||
*/
|
||||
function searchRequests(workspaceId, query) {
|
||||
const allRequests = getAllRequests(workspaceId);
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
return allRequests.filter(req =>
|
||||
req.name.toLowerCase().includes(lowerQuery) ||
|
||||
req.path.toLowerCase().includes(lowerQuery) ||
|
||||
req.description.toLowerCase().includes(lowerQuery) ||
|
||||
(req.tags && req.tags.some(t => t.toLowerCase().includes(lowerQuery)))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environments
|
||||
*/
|
||||
function getEnvironments() {
|
||||
try {
|
||||
const data = localStorage.getItem(ENV_KEY);
|
||||
return data ? JSON.parse(data).map(deserializeEnvironment) : [];
|
||||
} catch (e) {
|
||||
console.error('Failed to load environments:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save environment
|
||||
*/
|
||||
function saveEnvironment(environment) {
|
||||
const environments = getEnvironments();
|
||||
const index = environments.findIndex(e => e.id === environment.id);
|
||||
|
||||
if (index >= 0) {
|
||||
environments[index] = environment;
|
||||
} else {
|
||||
environments.push(environment);
|
||||
}
|
||||
|
||||
localStorage.setItem(ENV_KEY, JSON.stringify(environments));
|
||||
return environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete environment
|
||||
*/
|
||||
function deleteEnvironment(environmentId) {
|
||||
let environments = getEnvironments();
|
||||
environments = environments.filter(e => e.id !== environmentId);
|
||||
localStorage.setItem(ENV_KEY, JSON.stringify(environments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active environment
|
||||
*/
|
||||
function getActiveEnvironment() {
|
||||
const environments = getEnvironments();
|
||||
return environments.find(e => e.isActive) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set active environment
|
||||
*/
|
||||
function setActiveEnvironment(environmentId) {
|
||||
const environments = getEnvironments();
|
||||
environments.forEach(e => e.isActive = e.id === environmentId);
|
||||
localStorage.setItem(ENV_KEY, JSON.stringify(environments));
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute variables in string
|
||||
*/
|
||||
function substituteVariables(str, environment) {
|
||||
if (!str || !environment) return str;
|
||||
|
||||
let result = str;
|
||||
Object.entries(environment.variables).forEach(([name, value]) => {
|
||||
result = result.replace(new RegExp(`\\$\\{${name}\\}|\\$${name}\\b`, 'g'), String(value || ''));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export workspace as JSON
|
||||
*/
|
||||
function exportWorkspace(workspaceId) {
|
||||
const workspace = getWorkspaces().find(w => w.id === workspaceId);
|
||||
if (!workspace) return null;
|
||||
|
||||
return {
|
||||
version: '1.0',
|
||||
exported: new Date().toISOString(),
|
||||
workspace: workspace,
|
||||
environments: getEnvironments()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import workspace from JSON
|
||||
*/
|
||||
function importWorkspace(json) {
|
||||
try {
|
||||
const data = typeof json === 'string' ? JSON.parse(json) : json;
|
||||
|
||||
// Validate version
|
||||
if (!data.version) throw new Error('Invalid format: missing version');
|
||||
|
||||
// Create new workspace with imported data
|
||||
const workspace = data.workspace;
|
||||
workspace.id = generateId();
|
||||
workspace.createdAt = Date.now();
|
||||
workspace.updatedAt = Date.now();
|
||||
|
||||
// Import environments
|
||||
if (data.environments && Array.isArray(data.environments)) {
|
||||
data.environments.forEach(env => {
|
||||
env.id = generateId();
|
||||
saveEnvironment(env);
|
||||
});
|
||||
}
|
||||
|
||||
// Save workspace
|
||||
return saveWorkspace(workspace);
|
||||
} catch (e) {
|
||||
console.error('Failed to import workspace:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate request
|
||||
*/
|
||||
function duplicateRequest(request) {
|
||||
const duplicate = request.clone();
|
||||
duplicate.name = `${request.name} (copy)`;
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate collection
|
||||
*/
|
||||
function duplicateCollection(collection) {
|
||||
const duplicate = new Collection(collection.name + ' (copy)');
|
||||
duplicate.description = collection.description;
|
||||
duplicate.requests = collection.requests.map(r => r.clone ? r.clone() : r);
|
||||
if (collection.folders) {
|
||||
duplicate.folders = collection.folders.map(f => duplicateCollection(f));
|
||||
}
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent requests
|
||||
*/
|
||||
function getRecentRequests(workspaceId, limit = 10) {
|
||||
return getAllRequests(workspaceId)
|
||||
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize workspace (helper for JSON parsing)
|
||||
*/
|
||||
function deserializeWorkspace(data) {
|
||||
const ws = Object.assign(new Workspace(data.name), data);
|
||||
return ws;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize environment (helper for JSON parsing)
|
||||
*/
|
||||
function deserializeEnvironment(data) {
|
||||
const env = Object.assign(new Environment(data.name), data);
|
||||
return env;
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
Workspace,
|
||||
Collection,
|
||||
SavedRequest,
|
||||
Environment,
|
||||
generateId,
|
||||
getWorkspaces,
|
||||
getCurrentWorkspace,
|
||||
saveWorkspace,
|
||||
deleteWorkspace,
|
||||
addCollection,
|
||||
saveRequestToCollection,
|
||||
getAllRequests,
|
||||
searchRequests,
|
||||
getEnvironments,
|
||||
saveEnvironment,
|
||||
deleteEnvironment,
|
||||
getActiveEnvironment,
|
||||
setActiveEnvironment,
|
||||
substituteVariables,
|
||||
exportWorkspace,
|
||||
importWorkspace,
|
||||
duplicateRequest,
|
||||
duplicateCollection,
|
||||
getRecentRequests
|
||||
};
|
||||
})();
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = WorkspaceManager;
|
||||
}
|
||||
Reference in New Issue
Block a user