This commit is contained in:
2025-01-04 00:34:03 +01:00
parent 41829408dc
commit 0ca14bbc19
18111 changed files with 1871397 additions and 0 deletions

View File

@@ -0,0 +1 @@
export const debugId = "endpoints";

View File

@@ -0,0 +1,2 @@
export * from "./debugId";
export * from "./toDebugString";

View File

@@ -0,0 +1,12 @@
export function toDebugString(input) {
if (typeof input !== "object" || input == null) {
return input;
}
if ("ref" in input) {
return `$${toDebugString(input.ref)}`;
}
if ("fn" in input) {
return `${input.fn}(${(input.argv || []).map(toDebugString).join(", ")})`;
}
return JSON.stringify(input, null, 2);
}

View File

@@ -0,0 +1,21 @@
const ENV_ENDPOINT_URL = "AWS_ENDPOINT_URL";
const CONFIG_ENDPOINT_URL = "endpoint_url";
export const getEndpointUrlConfig = (serviceId) => ({
environmentVariableSelector: (env) => {
const serviceEndpointUrlSections = [ENV_ENDPOINT_URL, ...serviceId.split(" ").map((w) => w.toUpperCase())];
const serviceEndpointUrl = env[serviceEndpointUrlSections.join("_")];
if (serviceEndpointUrl)
return serviceEndpointUrl;
const endpointUrl = env[ENV_ENDPOINT_URL];
if (endpointUrl)
return endpointUrl;
return undefined;
},
configFileSelector: (profile) => {
const endpointUrl = profile[CONFIG_ENDPOINT_URL];
if (endpointUrl)
return endpointUrl;
return undefined;
},
default: undefined,
});

View File

@@ -0,0 +1,5 @@
export * from "./lib/isIpAddress";
export * from "./lib/isValidHostLabel";
export * from "./utils/customEndpointFunctions";
export * from "./resolveEndpoint";
export * from "./types";

View File

@@ -0,0 +1 @@
export const booleanEquals = (value1, value2) => value1 === value2;

View File

@@ -0,0 +1,11 @@
import { EndpointError } from "../types";
import { getAttrPathList } from "./getAttrPathList";
export const getAttr = (value, path) => getAttrPathList(path).reduce((acc, index) => {
if (typeof acc !== "object") {
throw new EndpointError(`Index '${index}' in '${path}' not found in '${JSON.stringify(value)}'`);
}
else if (Array.isArray(acc)) {
return acc[parseInt(index)];
}
return acc[index];
}, value);

View File

@@ -0,0 +1,25 @@
import { EndpointError } from "../types";
export const getAttrPathList = (path) => {
const parts = path.split(".");
const pathList = [];
for (const part of parts) {
const squareBracketIndex = part.indexOf("[");
if (squareBracketIndex !== -1) {
if (part.indexOf("]") !== part.length - 1) {
throw new EndpointError(`Path: '${path}' does not end with ']'`);
}
const arrayIndex = part.slice(squareBracketIndex + 1, -1);
if (Number.isNaN(parseInt(arrayIndex))) {
throw new EndpointError(`Invalid array index: '${arrayIndex}' in path: '${path}'`);
}
if (squareBracketIndex !== 0) {
pathList.push(part.slice(0, squareBracketIndex));
}
pathList.push(arrayIndex);
}
else {
pathList.push(part);
}
}
return pathList;
};

View File

@@ -0,0 +1,9 @@
export * from "./booleanEquals";
export * from "./getAttr";
export * from "./isSet";
export * from "./isValidHostLabel";
export * from "./not";
export * from "./parseURL";
export * from "./stringEquals";
export * from "./substring";
export * from "./uriEncode";

View File

@@ -0,0 +1,2 @@
const IP_V4_REGEX = new RegExp(`^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}$`);
export const isIpAddress = (value) => IP_V4_REGEX.test(value) || (value.startsWith("[") && value.endsWith("]"));

View File

@@ -0,0 +1 @@
export const isSet = (value) => value != null;

View File

@@ -0,0 +1,13 @@
const VALID_HOST_LABEL_REGEX = new RegExp(`^(?!.*-$)(?!-)[a-zA-Z0-9-]{1,63}$`);
export const isValidHostLabel = (value, allowSubDomains = false) => {
if (!allowSubDomains) {
return VALID_HOST_LABEL_REGEX.test(value);
}
const labels = value.split(".");
for (const label of labels) {
if (!isValidHostLabel(label)) {
return false;
}
}
return true;
};

View File

@@ -0,0 +1 @@
export const not = (value) => !value;

View File

@@ -0,0 +1,51 @@
import { EndpointURLScheme } from "@smithy/types";
import { isIpAddress } from "./isIpAddress";
const DEFAULT_PORTS = {
[EndpointURLScheme.HTTP]: 80,
[EndpointURLScheme.HTTPS]: 443,
};
export const parseURL = (value) => {
const whatwgURL = (() => {
try {
if (value instanceof URL) {
return value;
}
if (typeof value === "object" && "hostname" in value) {
const { hostname, port, protocol = "", path = "", query = {} } = value;
const url = new URL(`${protocol}//${hostname}${port ? `:${port}` : ""}${path}`);
url.search = Object.entries(query)
.map(([k, v]) => `${k}=${v}`)
.join("&");
return url;
}
return new URL(value);
}
catch (error) {
return null;
}
})();
if (!whatwgURL) {
console.error(`Unable to parse ${JSON.stringify(value)} as a whatwg URL.`);
return null;
}
const urlString = whatwgURL.href;
const { host, hostname, pathname, protocol, search } = whatwgURL;
if (search) {
return null;
}
const scheme = protocol.slice(0, -1);
if (!Object.values(EndpointURLScheme).includes(scheme)) {
return null;
}
const isIp = isIpAddress(hostname);
const inputContainsDefaultPort = urlString.includes(`${host}:${DEFAULT_PORTS[scheme]}`) ||
(typeof value === "string" && value.includes(`${host}:${DEFAULT_PORTS[scheme]}`));
const authority = `${host}${inputContainsDefaultPort ? `:${DEFAULT_PORTS[scheme]}` : ``}`;
return {
scheme,
authority,
path: pathname,
normalizedPath: pathname.endsWith("/") ? pathname : `${pathname}/`,
isIp,
};
};

View File

@@ -0,0 +1 @@
export const stringEquals = (value1, value2) => value1 === value2;

View File

@@ -0,0 +1,9 @@
export const substring = (input, start, stop, reverse) => {
if (start >= stop || input.length < stop) {
return null;
}
if (!reverse) {
return input.substring(start, stop);
}
return input.substring(input.length - stop, input.length - start);
};

View File

@@ -0,0 +1 @@
export const uriEncode = (value) => encodeURIComponent(value).replace(/[!*'()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);

View File

@@ -0,0 +1,37 @@
import { debugId, toDebugString } from "./debug";
import { EndpointError } from "./types";
import { evaluateRules } from "./utils";
export const resolveEndpoint = (ruleSetObject, options) => {
const { endpointParams, logger } = options;
const { parameters, rules } = ruleSetObject;
options.logger?.debug?.(`${debugId} Initial EndpointParams: ${toDebugString(endpointParams)}`);
const paramsWithDefault = Object.entries(parameters)
.filter(([, v]) => v.default != null)
.map(([k, v]) => [k, v.default]);
if (paramsWithDefault.length > 0) {
for (const [paramKey, paramDefaultValue] of paramsWithDefault) {
endpointParams[paramKey] = endpointParams[paramKey] ?? paramDefaultValue;
}
}
const requiredParams = Object.entries(parameters)
.filter(([, v]) => v.required)
.map(([k]) => k);
for (const requiredParam of requiredParams) {
if (endpointParams[requiredParam] == null) {
throw new EndpointError(`Missing required parameter: '${requiredParam}'`);
}
}
const endpoint = evaluateRules(rules, { endpointParams, logger, referenceRecord: {} });
if (options.endpointParams?.Endpoint) {
try {
const givenEndpoint = new URL(options.endpointParams.Endpoint);
const { protocol, port } = givenEndpoint;
endpoint.url.protocol = protocol;
endpoint.url.port = port;
}
catch (e) {
}
}
options.logger?.debug?.(`${debugId} Resolved endpoint: ${toDebugString(endpoint)}`);
return endpoint;
};

View File

@@ -0,0 +1,6 @@
export class EndpointError extends Error {
constructor(message) {
super(message);
this.name = "EndpointError";
}
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,7 @@
export * from "./EndpointError";
export * from "./EndpointFunctions";
export * from "./EndpointRuleObject";
export * from "./ErrorRuleObject";
export * from "./RuleSetObject";
export * from "./TreeRuleObject";
export * from "./shared";

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,11 @@
import { customEndpointFunctions } from "./customEndpointFunctions";
import { endpointFunctions } from "./endpointFunctions";
import { evaluateExpression } from "./evaluateExpression";
export const callFunction = ({ fn, argv }, options) => {
const evaluatedArgs = argv.map((arg) => ["boolean", "number"].includes(typeof arg) ? arg : evaluateExpression(arg, "arg", options));
const fnSegments = fn.split(".");
if (fnSegments[0] in customEndpointFunctions && fnSegments[1] != null) {
return customEndpointFunctions[fnSegments[0]][fnSegments[1]](...evaluatedArgs);
}
return endpointFunctions[fn](...evaluatedArgs);
};

View File

@@ -0,0 +1 @@
export const customEndpointFunctions = {};

View File

@@ -0,0 +1,12 @@
import { booleanEquals, getAttr, isSet, isValidHostLabel, not, parseURL, stringEquals, substring, uriEncode, } from "../lib";
export const endpointFunctions = {
booleanEquals,
getAttr,
isSet,
isValidHostLabel,
not,
parseURL,
stringEquals,
substring,
uriEncode,
};

View File

@@ -0,0 +1,14 @@
import { debugId, toDebugString } from "../debug";
import { EndpointError } from "../types";
import { callFunction } from "./callFunction";
export const evaluateCondition = ({ assign, ...fnArgs }, options) => {
if (assign && assign in options.referenceRecord) {
throw new EndpointError(`'${assign}' is already defined in Reference Record.`);
}
const value = callFunction(fnArgs, options);
options.logger?.debug?.(`${debugId} evaluateCondition: ${toDebugString(fnArgs)} = ${toDebugString(value)}`);
return {
result: value === "" ? true : !!value,
...(assign != null && { toAssign: { name: assign, value } }),
};
};

View File

@@ -0,0 +1,22 @@
import { debugId, toDebugString } from "../debug";
import { evaluateCondition } from "./evaluateCondition";
export const evaluateConditions = (conditions = [], options) => {
const conditionsReferenceRecord = {};
for (const condition of conditions) {
const { result, toAssign } = evaluateCondition(condition, {
...options,
referenceRecord: {
...options.referenceRecord,
...conditionsReferenceRecord,
},
});
if (!result) {
return { result };
}
if (toAssign) {
conditionsReferenceRecord[toAssign.name] = toAssign.value;
options.logger?.debug?.(`${debugId} assign: ${toAssign.name} := ${toDebugString(toAssign.value)}`);
}
}
return { result: true, referenceRecord: conditionsReferenceRecord };
};

View File

@@ -0,0 +1,27 @@
import { debugId, toDebugString } from "../debug";
import { evaluateConditions } from "./evaluateConditions";
import { getEndpointHeaders } from "./getEndpointHeaders";
import { getEndpointProperties } from "./getEndpointProperties";
import { getEndpointUrl } from "./getEndpointUrl";
export const evaluateEndpointRule = (endpointRule, options) => {
const { conditions, endpoint } = endpointRule;
const { result, referenceRecord } = evaluateConditions(conditions, options);
if (!result) {
return;
}
const endpointRuleOptions = {
...options,
referenceRecord: { ...options.referenceRecord, ...referenceRecord },
};
const { url, properties, headers } = endpoint;
options.logger?.debug?.(`${debugId} Resolving endpoint from template: ${toDebugString(endpoint)}`);
return {
...(headers != undefined && {
headers: getEndpointHeaders(headers, endpointRuleOptions),
}),
...(properties != undefined && {
properties: getEndpointProperties(properties, endpointRuleOptions),
}),
url: getEndpointUrl(url, endpointRuleOptions),
};
};

View File

@@ -0,0 +1,14 @@
import { EndpointError } from "../types";
import { evaluateConditions } from "./evaluateConditions";
import { evaluateExpression } from "./evaluateExpression";
export const evaluateErrorRule = (errorRule, options) => {
const { conditions, error } = errorRule;
const { result, referenceRecord } = evaluateConditions(conditions, options);
if (!result) {
return;
}
throw new EndpointError(evaluateExpression(error, "Error", {
...options,
referenceRecord: { ...options.referenceRecord, ...referenceRecord },
}));
};

View File

@@ -0,0 +1,16 @@
import { EndpointError } from "../types";
import { callFunction } from "./callFunction";
import { evaluateTemplate } from "./evaluateTemplate";
import { getReferenceValue } from "./getReferenceValue";
export const evaluateExpression = (obj, keyName, options) => {
if (typeof obj === "string") {
return evaluateTemplate(obj, options);
}
else if (obj["fn"]) {
return callFunction(obj, options);
}
else if (obj["ref"]) {
return getReferenceValue(obj, options);
}
throw new EndpointError(`'${keyName}': ${String(obj)} is not a string, function or reference.`);
};

View File

@@ -0,0 +1,27 @@
import { EndpointError } from "../types";
import { evaluateEndpointRule } from "./evaluateEndpointRule";
import { evaluateErrorRule } from "./evaluateErrorRule";
import { evaluateTreeRule } from "./evaluateTreeRule";
export const evaluateRules = (rules, options) => {
for (const rule of rules) {
if (rule.type === "endpoint") {
const endpointOrUndefined = evaluateEndpointRule(rule, options);
if (endpointOrUndefined) {
return endpointOrUndefined;
}
}
else if (rule.type === "error") {
evaluateErrorRule(rule, options);
}
else if (rule.type === "tree") {
const endpointOrUndefined = evaluateTreeRule(rule, options);
if (endpointOrUndefined) {
return endpointOrUndefined;
}
}
else {
throw new EndpointError(`Unknown endpoint rule: ${rule}`);
}
}
throw new EndpointError(`Rules evaluation failed`);
};

View File

@@ -0,0 +1,36 @@
import { getAttr } from "../lib";
export const evaluateTemplate = (template, options) => {
const evaluatedTemplateArr = [];
const templateContext = {
...options.endpointParams,
...options.referenceRecord,
};
let currentIndex = 0;
while (currentIndex < template.length) {
const openingBraceIndex = template.indexOf("{", currentIndex);
if (openingBraceIndex === -1) {
evaluatedTemplateArr.push(template.slice(currentIndex));
break;
}
evaluatedTemplateArr.push(template.slice(currentIndex, openingBraceIndex));
const closingBraceIndex = template.indexOf("}", openingBraceIndex);
if (closingBraceIndex === -1) {
evaluatedTemplateArr.push(template.slice(openingBraceIndex));
break;
}
if (template[openingBraceIndex + 1] === "{" && template[closingBraceIndex + 1] === "}") {
evaluatedTemplateArr.push(template.slice(openingBraceIndex + 1, closingBraceIndex));
currentIndex = closingBraceIndex + 2;
}
const parameterName = template.substring(openingBraceIndex + 1, closingBraceIndex);
if (parameterName.includes("#")) {
const [refName, attrName] = parameterName.split("#");
evaluatedTemplateArr.push(getAttr(templateContext[refName], attrName));
}
else {
evaluatedTemplateArr.push(templateContext[parameterName]);
}
currentIndex = closingBraceIndex + 1;
}
return evaluatedTemplateArr.join("");
};

View File

@@ -0,0 +1,13 @@
import { evaluateConditions } from "./evaluateConditions";
import { evaluateRules } from "./evaluateRules";
export const evaluateTreeRule = (treeRule, options) => {
const { conditions, rules } = treeRule;
const { result, referenceRecord } = evaluateConditions(conditions, options);
if (!result) {
return;
}
return evaluateRules(rules, {
...options,
referenceRecord: { ...options.referenceRecord, ...referenceRecord },
});
};

View File

@@ -0,0 +1,12 @@
import { EndpointError } from "../types";
import { evaluateExpression } from "./evaluateExpression";
export const getEndpointHeaders = (headers, options) => Object.entries(headers).reduce((acc, [headerKey, headerVal]) => ({
...acc,
[headerKey]: headerVal.map((headerValEntry) => {
const processedExpr = evaluateExpression(headerValEntry, "Header value entry", options);
if (typeof processedExpr !== "string") {
throw new EndpointError(`Header '${headerKey}' value '${processedExpr}' is not a string`);
}
return processedExpr;
}),
}), {});

View File

@@ -0,0 +1,5 @@
import { getEndpointProperty } from "./getEndpointProperty";
export const getEndpointProperties = (properties, options) => Object.entries(properties).reduce((acc, [propertyKey, propertyVal]) => ({
...acc,
[propertyKey]: getEndpointProperty(propertyVal, options),
}), {});

View File

@@ -0,0 +1,21 @@
import { EndpointError } from "../types";
import { evaluateTemplate } from "./evaluateTemplate";
import { getEndpointProperties } from "./getEndpointProperties";
export const getEndpointProperty = (property, options) => {
if (Array.isArray(property)) {
return property.map((propertyEntry) => getEndpointProperty(propertyEntry, options));
}
switch (typeof property) {
case "string":
return evaluateTemplate(property, options);
case "object":
if (property === null) {
throw new EndpointError(`Unexpected endpoint property: ${property}`);
}
return getEndpointProperties(property, options);
case "boolean":
return property;
default:
throw new EndpointError(`Unexpected endpoint property type: ${typeof property}`);
}
};

View File

@@ -0,0 +1,15 @@
import { EndpointError } from "../types";
import { evaluateExpression } from "./evaluateExpression";
export const getEndpointUrl = (endpointUrl, options) => {
const expression = evaluateExpression(endpointUrl, "Endpoint URL", options);
if (typeof expression === "string") {
try {
return new URL(expression);
}
catch (error) {
console.error(`Failed to construct URL with ${expression}`, error);
throw error;
}
}
throw new EndpointError(`Endpoint URL must be a string, got ${typeof expression}`);
};

View File

@@ -0,0 +1,7 @@
export const getReferenceValue = ({ ref }, options) => {
const referenceRecord = {
...options.endpointParams,
...options.referenceRecord,
};
return referenceRecord[ref];
};

View File

@@ -0,0 +1,2 @@
export * from "./customEndpointFunctions";
export * from "./evaluateRules";