400 lines
14 KiB
JavaScript
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;
|
|
}
|