Files
2026-06-24 07:04:59 +02:00

400 lines
14 KiB
JavaScript

/**
* 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;
}